1use crate::{
13 protocol::{self, request::InstallSource, Cohort},
14 storage::Storage,
15 time::PartialComplexTime,
16 version::Version,
17};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::fmt;
21use std::time::Duration;
22use tracing::error;
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 {
95 cohort: app.cohort.clone(),
96 user_counting: app.user_counting.clone(),
97 }
98 }
99}
100
101impl App {
102 pub async fn load<'a>(&'a mut self, storage: &'a impl Storage) {
104 if let Some(app_json) = storage.get_string(&self.id).await {
105 match serde_json::from_str::<PersistedApp>(&app_json) {
106 Ok(persisted_app) => {
107 if self.cohort.id.is_none() {
109 self.cohort.id = persisted_app.cohort.id;
110 }
111 if self.cohort.hint.is_none() {
112 self.cohort.hint = persisted_app.cohort.hint;
113 }
114 if self.cohort.name.is_none() {
115 self.cohort.name = persisted_app.cohort.name;
116 }
117 if self.user_counting == UserCounting::ClientRegulatedByDate(None) {
118 self.user_counting = persisted_app.user_counting;
119 }
120 }
121 Err(e) => {
122 error!(
123 "Unable to deserialize PersistedApp from json {}: {}",
124 app_json, e
125 );
126 }
127 }
128 }
129 }
130
131 pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) {
135 let persisted_app = PersistedApp::from(self);
136 match serde_json::to_string(&persisted_app) {
137 Ok(json) => {
138 if let Err(e) = storage.set_string(&self.id, &json).await {
139 error!("Unable to persist cohort id: {}", e);
140 }
141 }
142 Err(e) => {
143 error!(
144 "Unable to serialize PersistedApp {:?}: {}",
145 persisted_app, e
146 );
147 }
148 }
149 }
150
151 pub fn get_current_channel(&self) -> &str {
154 self.cohort.name.as_deref().unwrap_or("")
155 }
156
157 pub fn get_target_channel(&self) -> &str {
159 self.cohort
160 .hint
161 .as_deref()
162 .unwrap_or_else(|| self.get_current_channel())
163 }
164
165 pub fn set_target_channel(&mut self, channel: Option<String>, id: Option<String>) {
167 self.cohort.hint = channel;
168 if let Some(id) = id {
169 self.id = id;
170 }
171 }
172
173 pub fn valid(&self) -> bool {
174 !self.id.is_empty() && self.version != Version::from([0])
175 }
176}
177
178#[derive(Clone, Debug, Default, PartialEq, Eq)]
180pub struct CheckOptions {
181 pub source: InstallSource,
185}
186
187#[derive(Clone, Copy, Default, PartialEq, Eq, TypedBuilder)]
189pub struct UpdateCheckSchedule {
190 #[builder(default, setter(into))]
196 pub last_update_time: Option<PartialComplexTime>,
197
198 #[builder(default, setter(into))]
200 pub last_update_check_time: Option<PartialComplexTime>,
201
202 #[builder(default, setter(into))]
204 pub next_update_time: Option<CheckTiming>,
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq, TypedBuilder)]
212pub struct CheckTiming {
213 #[builder(setter(into))]
216 pub time: PartialComplexTime,
217
218 #[builder(default, setter(strip_option))]
222 pub minimum_wait: Option<Duration>,
223}
224
225pub struct PrettyOptionDisplay<T>(pub Option<T>)
234where
235 T: fmt::Display;
236impl<T> fmt::Display for PrettyOptionDisplay<T>
237where
238 T: fmt::Display,
239{
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 match &self.0 {
242 None => write!(f, "None"),
243 Some(value) => fmt::Display::fmt(value, f),
244 }
245 }
246}
247impl<T> fmt::Debug for PrettyOptionDisplay<T>
248where
249 T: fmt::Display,
250{
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 fmt::Display::fmt(self, f)
253 }
254}
255
256impl fmt::Debug for UpdateCheckSchedule {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 f.debug_struct("UpdateCheckSchedule")
265 .field(
266 "last_update_time",
267 &PrettyOptionDisplay(self.last_update_time),
268 )
269 .field(
270 "next_update_time",
271 &PrettyOptionDisplay(self.next_update_time),
272 )
273 .finish()
274 }
275}
276
277impl fmt::Display for CheckTiming {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 match self.minimum_wait {
280 None => fmt::Display::fmt(&self.time, f),
281 Some(wait) => write!(f, "{} wait: {:?}", &self.time, &wait),
282 }
283 }
284}
285
286#[derive(Clone, Debug, Default, Eq, PartialEq)]
290pub struct ProtocolState {
291 pub server_dictated_poll_interval: Option<std::time::Duration>,
294
295 pub consecutive_failed_update_checks: u32,
297
298 pub consecutive_proxied_requests: u32,
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::{
307 storage::MemStorage,
308 time::{MockTimeSource, TimeSource},
309 };
310 use futures::executor::block_on;
311 use pretty_assertions::assert_eq;
312 use std::str::FromStr;
313 use std::time::SystemTime;
314
315 #[test]
316 fn test_app_new_version() {
317 let app = App::builder()
318 .id("some_id")
319 .version([1, 2])
320 .cohort(Cohort::from_hint("some-channel"))
321 .build();
322 assert_eq!(app.id, "some_id");
323 assert_eq!(app.version, [1, 2].into());
324 assert_eq!(app.fingerprint, None);
325 assert_eq!(app.cohort.hint, Some("some-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_fingerprint() {
334 let app = App::builder()
335 .id("some_id_2")
336 .version([4, 6])
337 .cohort(Cohort::from_hint("test-channel"))
338 .fingerprint("some_fp")
339 .build();
340 assert_eq!(app.id, "some_id_2");
341 assert_eq!(app.version, [4, 6].into());
342 assert_eq!(app.fingerprint, Some("some_fp".to_string()));
343 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
344 assert_eq!(app.cohort.name, None);
345 assert_eq!(app.cohort.id, None);
346 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
347 assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
348 }
349
350 #[test]
351 fn test_app_with_user_counting() {
352 let app = App::builder()
353 .id("some_id_2")
354 .version([4, 6])
355 .cohort(Cohort::from_hint("test-channel"))
356 .user_counting(UserCounting::ClientRegulatedByDate(Some(42)))
357 .build();
358 assert_eq!(app.id, "some_id_2");
359 assert_eq!(app.version, [4, 6].into());
360 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
361 assert_eq!(app.cohort.name, None);
362 assert_eq!(app.cohort.id, None);
363 assert_eq!(
364 app.user_counting,
365 UserCounting::ClientRegulatedByDate(Some(42))
366 );
367 assert!(app.extra_fields.is_empty(), "Extra fields are not empty");
368 }
369
370 #[test]
371 fn test_app_with_extras() {
372 let app = App::builder()
373 .id("some_id_2")
374 .version([4, 6])
375 .cohort(Cohort::from_hint("test-channel"))
376 .extra_fields([
377 ("key1".to_string(), "value1".to_string()),
378 ("key2".to_string(), "value2".to_string()),
379 ])
380 .build();
381 assert_eq!(app.id, "some_id_2");
382 assert_eq!(app.version, [4, 6].into());
383 assert_eq!(app.cohort.hint, Some("test-channel".to_string()));
384 assert_eq!(app.cohort.name, None);
385 assert_eq!(app.cohort.id, None);
386 assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None));
387 assert_eq!(app.extra_fields.len(), 2);
388 assert_eq!(app.extra_fields["key1"], "value1");
389 assert_eq!(app.extra_fields["key2"], "value2");
390 }
391
392 #[test]
393 fn test_app_load() {
394 block_on(async {
395 let mut storage = MemStorage::new();
396 let json = serde_json::json!({
397 "cohort": {
398 "cohort": "some_id",
399 "cohorthint":"some_hint",
400 "cohortname": "some_name"
401 },
402 "user_counting": {
403 "ClientRegulatedByDate":123
404 }});
405 let json = serde_json::to_string(&json).unwrap();
406 let mut app = App::builder().id("some_id").version([1, 2]).build();
407 storage.set_string(&app.id, &json).await.unwrap();
408 app.load(&storage).await;
409
410 let cohort = Cohort {
411 id: Some("some_id".to_string()),
412 hint: Some("some_hint".to_string()),
413 name: Some("some_name".to_string()),
414 };
415 assert_eq!(cohort, app.cohort);
416 assert_eq!(
417 UserCounting::ClientRegulatedByDate(Some(123)),
418 app.user_counting
419 );
420 });
421 }
422
423 #[test]
424 fn test_app_load_empty_storage() {
425 block_on(async {
426 let storage = MemStorage::new();
427 let cohort = Cohort {
428 id: Some("some_id".to_string()),
429 hint: Some("some_hint".to_string()),
430 name: Some("some_name".to_string()),
431 };
432 let mut app = App::builder()
433 .id("some_id")
434 .version([1, 2])
435 .cohort(cohort)
436 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
437 .build();
438 app.load(&storage).await;
439
440 let cohort = Cohort {
442 id: Some("some_id".to_string()),
443 hint: Some("some_hint".to_string()),
444 name: Some("some_name".to_string()),
445 };
446 assert_eq!(cohort, app.cohort);
447 assert_eq!(
448 UserCounting::ClientRegulatedByDate(Some(123)),
449 app.user_counting
450 );
451 });
452 }
453
454 #[test]
455 fn test_app_load_malformed() {
456 block_on(async {
457 let mut storage = MemStorage::new();
458 let cohort = Cohort {
459 id: Some("some_id".to_string()),
460 hint: Some("some_hint".to_string()),
461 name: Some("some_name".to_string()),
462 };
463 let mut app = App::builder()
464 .id("some_id")
465 .version([1, 2])
466 .cohort(cohort)
467 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
468 .build();
469 storage.set_string(&app.id, "not a json").await.unwrap();
470 app.load(&storage).await;
471
472 let cohort = Cohort {
474 id: Some("some_id".to_string()),
475 hint: Some("some_hint".to_string()),
476 name: Some("some_name".to_string()),
477 };
478 assert_eq!(cohort, app.cohort);
479 assert_eq!(
480 UserCounting::ClientRegulatedByDate(Some(123)),
481 app.user_counting
482 );
483 });
484 }
485
486 #[test]
487 fn test_app_load_partial() {
488 block_on(async {
489 let mut storage = MemStorage::new();
490 let json = serde_json::json!({
491 "cohort": {
492 "cohorthint":"some_hint_2",
493 "cohortname": "some_name_2"
494 },
495 "user_counting": {
496 "ClientRegulatedByDate":null
497 }});
498 let json = serde_json::to_string(&json).unwrap();
499 let cohort = Cohort {
500 id: Some("some_id".to_string()),
501 hint: Some("some_hint".to_string()),
502 name: Some("some_name".to_string()),
503 };
504 let mut app = App::builder()
505 .id("some_id")
506 .version([1, 2])
507 .cohort(cohort)
508 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
509 .build();
510 storage.set_string(&app.id, &json).await.unwrap();
511 app.load(&storage).await;
512
513 let cohort = Cohort {
515 id: Some("some_id".to_string()),
516 hint: Some("some_hint".to_string()),
517 name: Some("some_name".to_string()),
518 };
519 assert_eq!(cohort, app.cohort);
520 assert_eq!(
521 UserCounting::ClientRegulatedByDate(Some(123)),
522 app.user_counting
523 );
524 });
525 }
526
527 #[test]
528 fn test_app_load_override() {
529 block_on(async {
530 let mut storage = MemStorage::new();
531 let json = serde_json::json!({
532 "cohort": {
533 "cohort": "some_id_2",
534 "cohorthint":"some_hint_2",
535 "cohortname": "some_name_2"
536 },
537 "user_counting": {
538 "ClientRegulatedByDate":123
539 }});
540 let json = serde_json::to_string(&json).unwrap();
541 let cohort = Cohort {
542 id: Some("some_id".to_string()),
543 hint: Some("some_hint".to_string()),
544 name: None,
545 };
546 let mut app = App::builder()
547 .id("some_id")
548 .version([1, 2])
549 .cohort(cohort)
550 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
551 .build();
552 storage.set_string(&app.id, &json).await.unwrap();
553 app.load(&storage).await;
554
555 let cohort = Cohort {
557 id: Some("some_id".to_string()),
558 hint: Some("some_hint".to_string()),
559 name: Some("some_name_2".to_string()),
560 };
561 assert_eq!(cohort, app.cohort);
562 assert_eq!(
563 UserCounting::ClientRegulatedByDate(Some(123)),
564 app.user_counting
565 );
566 });
567 }
568
569 #[test]
570 fn test_app_persist() {
571 block_on(async {
572 let mut storage = MemStorage::new();
573 let cohort = Cohort {
574 id: Some("some_id".to_string()),
575 hint: Some("some_hint".to_string()),
576 name: Some("some_name".to_string()),
577 };
578 let app = App::builder()
579 .id("some_id")
580 .version([1, 2])
581 .cohort(cohort)
582 .user_counting(UserCounting::ClientRegulatedByDate(Some(123)))
583 .build();
584 app.persist(&mut storage).await;
585
586 let expected = serde_json::json!({
587 "cohort": {
588 "cohort": "some_id",
589 "cohorthint":"some_hint",
590 "cohortname": "some_name"
591 },
592 "user_counting": {
593 "ClientRegulatedByDate":123
594 }});
595 let json = storage.get_string(&app.id).await.unwrap();
596 assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
597 assert!(!storage.committed());
598 });
599 }
600
601 #[test]
602 fn test_app_persist_empty() {
603 block_on(async {
604 let mut storage = MemStorage::new();
605 let cohort = Cohort {
606 id: None,
607 hint: None,
608 name: None,
609 };
610 let app = App::builder()
611 .id("some_id")
612 .version([1, 2])
613 .cohort(cohort)
614 .build();
615 app.persist(&mut storage).await;
616
617 let expected = serde_json::json!({
618 "cohort": {},
619 "user_counting": {
620 "ClientRegulatedByDate":null
621 }});
622 let json = storage.get_string(&app.id).await.unwrap();
623 assert_eq!(expected, serde_json::Value::from_str(&json).unwrap());
624 assert!(!storage.committed());
625 });
626 }
627
628 #[test]
629 fn test_app_get_current_channel() {
630 let cohort = Cohort {
631 name: Some("current-channel-123".to_string()),
632 ..Cohort::default()
633 };
634 let app = App::builder()
635 .id("some_id")
636 .version([0, 1])
637 .cohort(cohort)
638 .build();
639 assert_eq!("current-channel-123", app.get_current_channel());
640 }
641
642 #[test]
643 fn test_app_get_current_channel_default() {
644 let app = App::builder().id("some_id").version([0, 1]).build();
645 assert_eq!("", app.get_current_channel());
646 }
647
648 #[test]
649 fn test_app_get_target_channel() {
650 let cohort = Cohort::from_hint("target-channel-456");
651 let app = App::builder()
652 .id("some_id")
653 .version([0, 1])
654 .cohort(cohort)
655 .build();
656 assert_eq!("target-channel-456", app.get_target_channel());
657 }
658
659 #[test]
660 fn test_app_get_target_channel_fallback() {
661 let cohort = Cohort {
662 name: Some("current-channel-123".to_string()),
663 ..Cohort::default()
664 };
665 let app = App::builder()
666 .id("some_id")
667 .version([0, 1])
668 .cohort(cohort)
669 .build();
670 assert_eq!("current-channel-123", app.get_target_channel());
671 }
672
673 #[test]
674 fn test_app_get_target_channel_default() {
675 let app = App::builder().id("some_id").version([0, 1]).build();
676 assert_eq!("", app.get_target_channel());
677 }
678
679 #[test]
680 fn test_app_set_target_channel() {
681 let mut app = App::builder().id("some_id").version([0, 1]).build();
682 assert_eq!("", app.get_target_channel());
683 app.set_target_channel(Some("new-target-channel".to_string()), None);
684 assert_eq!("new-target-channel", app.get_target_channel());
685 app.set_target_channel(None, None);
686 assert_eq!("", app.get_target_channel());
687 }
688
689 #[test]
690 fn test_app_set_target_channel_and_id() {
691 let mut app = App::builder().id("some_id").version([0, 1]).build();
692 assert_eq!("", app.get_target_channel());
693 app.set_target_channel(
694 Some("new-target-channel".to_string()),
695 Some("new-id".to_string()),
696 );
697 assert_eq!("new-target-channel", app.get_target_channel());
698 assert_eq!("new-id", app.id);
699 app.set_target_channel(None, None);
700 assert_eq!("", app.get_target_channel());
701 assert_eq!("new-id", app.id);
702 }
703
704 #[test]
705 fn test_app_valid() {
706 let app = App::builder().id("some_id").version([0, 1]).build();
707 assert!(app.valid());
708 }
709
710 #[test]
711 fn test_app_not_valid() {
712 let app = App::builder().id("").version([0, 1]).build();
713 assert!(!app.valid());
714 let app = App::builder().id("some_id").version([0]).build();
715 assert!(!app.valid());
716 }
717
718 #[test]
719 fn test_pretty_option_display_with_none() {
720 assert_eq!(
721 "None",
722 format!("{:?}", PrettyOptionDisplay(Option::<String>::None))
723 );
724 }
725
726 #[test]
727 fn test_pretty_option_display_with_some() {
728 assert_eq!(
729 "this is a test",
730 format!("{:?}", PrettyOptionDisplay(Some("this is a test")))
731 );
732 }
733
734 #[test]
735 fn test_update_check_schedule_debug_with_defaults() {
736 assert_eq!(
737 "UpdateCheckSchedule { \
738 last_update_time: None, \
739 next_update_time: None \
740 }",
741 format!("{:?}", UpdateCheckSchedule::default())
742 );
743 }
744
745 #[test]
746 fn test_update_check_schedule_debug_with_values() {
747 let mock_time = MockTimeSource::new_from_now();
748 let last = mock_time.now();
749 let next = last + Duration::from_secs(1000);
750 assert_eq!(
751 format!(
752 "UpdateCheckSchedule {{ last_update_time: {}, next_update_time: {} }}",
753 PartialComplexTime::from(last),
754 next
755 ),
756 format!(
757 "{:?}",
758 UpdateCheckSchedule::builder()
759 .last_update_time(last)
760 .next_update_time(CheckTiming::builder().time(next).build())
761 .build()
762 )
763 );
764 }
765
766 #[test]
767 fn test_update_check_schedule_builder_all_fields() {
768 let mock_time = MockTimeSource::new_from_now();
769 let now = PartialComplexTime::from(mock_time.now());
770 assert_eq!(
771 UpdateCheckSchedule::builder()
772 .last_update_time(PartialComplexTime::from(
773 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
774 ))
775 .next_update_time(
776 CheckTiming::builder()
777 .time(now)
778 .minimum_wait(Duration::from_secs(100))
779 .build()
780 )
781 .build(),
782 UpdateCheckSchedule {
783 last_update_time: Some(PartialComplexTime::from(
784 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
785 )),
786 next_update_time: Some(CheckTiming {
787 time: now,
788 minimum_wait: Some(Duration::from_secs(100))
789 }),
790 ..Default::default()
791 }
792 );
793 }
794
795 #[test]
796 fn test_update_check_schedule_builder_all_fields_from_options() {
797 let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
798 assert_eq!(
799 UpdateCheckSchedule::builder()
800 .last_update_time(Some(PartialComplexTime::from(
801 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
802 )))
803 .next_update_time(Some(
804 CheckTiming::builder()
805 .time(next_time)
806 .minimum_wait(Duration::from_secs(100))
807 .build()
808 ))
809 .build(),
810 UpdateCheckSchedule {
811 last_update_time: Some(PartialComplexTime::from(
812 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
813 )),
814 next_update_time: Some(CheckTiming {
815 time: next_time,
816 minimum_wait: Some(Duration::from_secs(100))
817 }),
818 ..Default::default()
819 }
820 );
821 }
822
823 #[test]
824 fn test_update_check_schedule_builder_subset_fields() {
825 assert_eq!(
826 UpdateCheckSchedule::builder()
827 .last_update_time(PartialComplexTime::from(
828 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
829 ))
830 .build(),
831 UpdateCheckSchedule {
832 last_update_time: Some(PartialComplexTime::from(
833 SystemTime::UNIX_EPOCH + Duration::from_secs(100000)
834 )),
835 ..Default::default()
836 }
837 );
838
839 let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now());
840 assert_eq!(
841 UpdateCheckSchedule::builder()
842 .next_update_time(
843 CheckTiming::builder()
844 .time(next_time)
845 .minimum_wait(Duration::from_secs(5))
846 .build()
847 )
848 .build(),
849 UpdateCheckSchedule {
850 next_update_time: Some(CheckTiming {
851 time: next_time,
852 minimum_wait: Some(Duration::from_secs(5))
853 }),
854 ..Default::default()
855 }
856 );
857 }
858
859 #[test]
860 fn test_update_check_schedule_builder_defaults_are_same_as_default_impl() {
861 assert_eq!(
862 UpdateCheckSchedule::builder().build(),
863 UpdateCheckSchedule::default()
864 );
865 }
866}