1mod error;
6
7pub use error::{ApiError, ApiResult, Error, Result};
8
9use chrono::{DateTime, Duration, Utc};
10use error_support::{error, handle_error};
11use parking_lot::{Mutex, RwLock};
12use uuid::Uuid;
13
14uniffi::setup_scaffolding!("context_id");
15
16mod callback;
17pub use callback::{ContextIdCallback, DefaultContextIdCallback};
18
19mod mars;
20use mars::{MARSClient, SimpleMARSClient};
21
22#[derive(uniffi::Object)]
24pub struct ContextIDComponent {
25 inner: Mutex<ContextIDComponentInner>,
26}
27
28#[uniffi::export]
29impl ContextIDComponent {
30 #[uniffi::constructor]
34
35 pub fn new(
36 init_context_id: &str,
37 creation_timestamp_s: i64,
38 running_in_test_automation: bool,
39 callback: Box<dyn ContextIdCallback>,
40 ) -> Self {
41 Self {
42 inner: Mutex::new(ContextIDComponentInner::new(
43 init_context_id,
44 creation_timestamp_s,
45 running_in_test_automation,
46 callback,
47 Utc::now(),
48 Box::new(SimpleMARSClient::new()),
49 )),
50 }
51 }
52
53 #[handle_error(Error)]
55 pub fn request(&self, rotation_days_in_s: u8) -> ApiResult<String> {
56 let mut inner = self.inner.lock();
57 inner.request(rotation_days_in_s, Utc::now())
58 }
59
60 #[handle_error(Error)]
62 pub fn force_rotation(&self) -> ApiResult<()> {
63 let mut inner = self.inner.lock();
64 inner.force_rotation(Utc::now());
65 Ok(())
66 }
67
68 #[handle_error(Error)]
71 pub fn unset_callback(&self) -> ApiResult<()> {
72 let mut inner = self.inner.lock();
73 inner.unset_callback();
74 Ok(())
75 }
76}
77
78struct ContextIDComponentInner {
79 context_id: String,
80 creation_timestamp: DateTime<Utc>,
81 callback_handle: RwLock<Box<dyn ContextIdCallback>>,
82 mars_client: Box<dyn MARSClient>,
83 running_in_test_automation: bool,
84}
85
86impl ContextIDComponentInner {
87 pub fn new(
88 init_context_id: &str,
89 creation_timestamp_s: i64,
90 running_in_test_automation: bool,
91 callback: Box<dyn ContextIdCallback>,
92 now: DateTime<Utc>,
93 mars_client: Box<dyn MARSClient>,
94 ) -> Self {
95 let (context_id, generated_context_id) = match init_context_id
99 .trim()
100 .trim_start_matches('{')
101 .trim_end_matches('}')
102 {
103 "" => (Uuid::new_v4().to_string(), true),
104 s => match Uuid::parse_str(s) {
107 Ok(_) => (s.to_string(), false),
108 Err(_) => (Uuid::new_v4().to_string(), true),
109 },
110 };
111
112 let (creation_timestamp, generated_creation_timestamp, force_rotation) =
113 match (generated_context_id, creation_timestamp_s) {
114 (true, _) => (now, true, false),
116 (false, secs) if secs > 0 => DateTime::<Utc>::from_timestamp(secs, 0)
121 .map(|ts| (ts, false, false))
122 .unwrap_or((now, true, true)),
123 (false, 0) => (now, true, false),
125 (false, _) => (now, true, true),
127 };
128
129 let mut instance = Self {
130 context_id,
131 creation_timestamp,
132 callback_handle: RwLock::new(callback),
133 mars_client,
134 running_in_test_automation,
135 };
136
137 if force_rotation {
138 instance.force_rotation(creation_timestamp)
140 } else if generated_context_id || generated_creation_timestamp {
141 instance.persist();
143 }
144
145 instance
146 }
147
148 pub fn request(&mut self, rotation_days: u8, now: DateTime<Utc>) -> Result<String> {
149 if rotation_days == 0 {
150 return Ok(self.context_id.clone());
151 }
152
153 let age = now - self.creation_timestamp;
154 if age >= Duration::days(rotation_days.into()) {
155 self.rotate_context_id(now);
156 }
157
158 Ok(self.context_id.clone())
159 }
160
161 fn rotate_context_id(&mut self, now: DateTime<Utc>) {
162 let original_context_id = self.context_id.clone();
163
164 self.context_id = Uuid::new_v4().to_string();
165 self.creation_timestamp = now;
166 self.persist();
167
168 if !self.running_in_test_automation {
171 let _ = self
172 .mars_client
173 .delete(original_context_id.clone())
174 .inspect_err(|e| error!("Failed to contact MARS: {}", e));
175 }
176
177 self.callback_handle
182 .read()
183 .rotated(original_context_id.clone());
184 }
185
186 pub fn force_rotation(&mut self, now: DateTime<Utc>) {
187 self.rotate_context_id(now);
188 }
189
190 pub fn persist(&self) {
191 self.callback_handle
192 .read()
193 .persist(self.context_id.clone(), self.creation_timestamp.timestamp());
194 }
195
196 pub fn unset_callback(&mut self) {
197 self.callback_handle = RwLock::new(Box::new(DefaultContextIdCallback));
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::callback::utils::SpyCallback;
205 use chrono::Utc;
206 use lazy_static::lazy_static;
207
208 use std::sync::{Arc, Mutex};
209
210 const FAKE_NOW_TS: i64 = 1745859061;
212 const TEST_CONTEXT_ID: &str = "decafbad-0cd1-0cd2-0cd3-decafbad1000";
213 const FAKE_LONG_AGO_TS: i64 = 1706763600;
215
216 lazy_static! {
217 static ref FAKE_NOW: DateTime<Utc> = DateTime::from_timestamp(FAKE_NOW_TS, 0).unwrap();
218 static ref FAKE_LONG_AGO: DateTime<Utc> =
219 DateTime::from_timestamp(FAKE_LONG_AGO_TS, 0).unwrap();
220 }
221
222 pub struct TestMARSClient {
223 delete_called: Arc<Mutex<bool>>,
224 }
225
226 impl TestMARSClient {
227 pub fn new(delete_called: Arc<Mutex<bool>>) -> Self {
228 Self { delete_called }
229 }
230 }
231
232 impl MARSClient for TestMARSClient {
233 fn delete(&self, _context_id: String) -> crate::Result<()> {
234 *self.delete_called.lock().unwrap() = true;
235 Ok(())
236 }
237 }
238
239 fn with_test_mars<F: FnOnce(Box<dyn MARSClient + Send + Sync>, Arc<Mutex<bool>>)>(test: F) {
240 let delete_called = Arc::new(Mutex::new(false));
241 let mars = Box::new(TestMARSClient::new(Arc::clone(&delete_called)));
242 test(mars, delete_called);
243 }
244
245 #[test]
246 fn test_creation_timestamp_with_some_value() {
247 with_test_mars(|mars, delete_called| {
248 let spy = SpyCallback::new();
249 let creation_timestamp = FAKE_NOW_TS;
250 let component = ContextIDComponentInner::new(
251 TEST_CONTEXT_ID,
252 creation_timestamp,
253 false,
254 spy.callback,
255 *FAKE_NOW,
256 mars,
257 );
258
259 assert_eq!(component.context_id, TEST_CONTEXT_ID);
262 assert_eq!(component.creation_timestamp.timestamp(), creation_timestamp);
263 assert!(!*delete_called.lock().unwrap());
264 assert_eq!(
266 *spy.persist_called.lock().unwrap(),
267 0,
268 "persist() should NOTE have been called"
269 );
270 assert_eq!(
271 *spy.rotated_called.lock().unwrap(),
272 0,
273 "rotated() should NOT have been called"
274 );
275 });
276 }
277
278 #[test]
279 fn test_creation_timestamp_with_zero_value() {
280 with_test_mars(|mars, delete_called| {
281 let spy = SpyCallback::new();
282 let component = ContextIDComponentInner::new(
283 TEST_CONTEXT_ID,
284 0,
285 false,
286 spy.callback,
287 *FAKE_NOW,
288 mars,
289 );
290
291 assert_eq!(component.context_id, TEST_CONTEXT_ID);
295 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
296 assert!(!*delete_called.lock().unwrap());
297 assert_eq!(
299 *spy.persist_called.lock().unwrap(),
300 1,
301 "persist() should have been called once"
302 );
303 assert_eq!(
304 *spy.rotated_called.lock().unwrap(),
305 0,
306 "rotated() should NOT have been called"
307 );
308 });
309 }
310
311 #[test]
312 fn test_empty_initial_context_id() {
313 with_test_mars(|mars, delete_called| {
314 let spy = SpyCallback::new();
315 let component =
316 ContextIDComponentInner::new("", 0, false, spy.callback, *FAKE_NOW, mars);
317
318 assert!(Uuid::parse_str(&component.context_id).is_ok());
320 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
321 assert_eq!(
322 *spy.persist_called.lock().unwrap(),
323 1,
324 "persist() should have been called once"
325 );
326 assert_eq!(
327 *spy.rotated_called.lock().unwrap(),
328 0,
329 "rotated() should NOT have been called"
330 );
331 assert!(!*delete_called.lock().unwrap());
332 });
333 }
334
335 #[test]
336 fn test_empty_initial_context_id_with_creation_date() {
337 with_test_mars(|mars, delete_called| {
338 let spy = SpyCallback::new();
339 let component =
340 ContextIDComponentInner::new("", 0, false, spy.callback, *FAKE_NOW, mars);
341
342 assert!(Uuid::parse_str(&component.context_id).is_ok());
344 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
345 assert!(!*delete_called.lock().unwrap());
346 assert_eq!(
347 *spy.persist_called.lock().unwrap(),
348 1,
349 "persist() should have been called once"
350 );
351 assert_eq!(
352 *spy.rotated_called.lock().unwrap(),
353 0,
354 "rotated() should NOT have been called"
355 );
356 });
357 }
358
359 #[test]
360 fn test_invalid_context_id_with_no_creation_date() {
361 with_test_mars(|mars, delete_called| {
362 let spy = SpyCallback::new();
363 let component = ContextIDComponentInner::new(
364 "something-invalid",
365 0,
366 false,
367 spy.callback,
368 *FAKE_NOW,
369 mars,
370 );
371
372 assert!(Uuid::parse_str(&component.context_id).is_ok());
374 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
375 assert_eq!(
377 *spy.persist_called.lock().unwrap(),
378 1,
379 "persist() should have been called once"
380 );
381 assert_eq!(
382 *spy.rotated_called.lock().unwrap(),
383 0,
384 "rotated() should NOT have been called"
385 );
386 assert!(!*delete_called.lock().unwrap());
387 });
388 }
389
390 #[test]
391 fn test_invalid_context_id_with_creation_date() {
392 with_test_mars(|mars, delete_called| {
393 let spy = SpyCallback::new();
394 let component = ContextIDComponentInner::new(
395 "something-invalid",
396 FAKE_LONG_AGO_TS,
397 false,
398 spy.callback,
399 *FAKE_NOW,
400 mars,
401 );
402
403 assert!(Uuid::parse_str(&component.context_id).is_ok());
405 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
406 assert_eq!(
408 *spy.persist_called.lock().unwrap(),
409 1,
410 "persist() should have been called once"
411 );
412 assert_eq!(
413 *spy.rotated_called.lock().unwrap(),
414 0,
415 "rotated() should NOT have been called"
416 );
417 assert!(!*delete_called.lock().unwrap());
418 });
419 }
420
421 #[test]
422 fn test_context_id_with_invalid_creation_date() {
423 with_test_mars(|mars, delete_called| {
424 let spy = SpyCallback::new();
425 let component = ContextIDComponentInner::new(
426 TEST_CONTEXT_ID,
427 -1,
428 false,
429 spy.callback,
430 *FAKE_NOW,
431 mars,
432 );
433
434 assert!(Uuid::parse_str(&component.context_id).is_ok());
436 assert_ne!(component.context_id, TEST_CONTEXT_ID);
437
438 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
440
441 assert_eq!(
443 *spy.persist_called.lock().unwrap(),
444 1,
445 "persist() should have been called once"
446 );
447 assert_eq!(
448 *spy.rotated_called.lock().unwrap(),
449 1,
450 "rotated() should have been called once"
451 );
452 assert!(*delete_called.lock().unwrap());
454 });
455 }
456
457 #[test]
458 fn test_context_id_with_out_of_range_creation_date() {
459 with_test_mars(|mars, delete_called| {
460 let spy = SpyCallback::new();
461 let huge_secs = i64::MAX;
463 let component = ContextIDComponentInner::new(
464 TEST_CONTEXT_ID,
465 huge_secs,
466 false,
467 spy.callback,
468 *FAKE_NOW,
469 mars,
470 );
471
472 assert!(Uuid::parse_str(&component.context_id).is_ok());
474 assert_ne!(component.context_id, TEST_CONTEXT_ID);
475
476 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
478
479 assert_eq!(
481 *spy.persist_called.lock().unwrap(),
482 1,
483 "persist() should have been called once"
484 );
485 assert_eq!(
486 *spy.rotated_called.lock().unwrap(),
487 1,
488 "rotated() should have been called once"
489 );
490 assert!(*delete_called.lock().unwrap());
492 });
493 }
494
495 #[test]
496 fn test_request_no_rotation() {
497 with_test_mars(|mars, delete_called| {
498 let mut component = ContextIDComponentInner::new(
500 TEST_CONTEXT_ID,
501 FAKE_LONG_AGO_TS,
502 false,
503 Box::new(DefaultContextIdCallback),
504 *FAKE_NOW,
505 mars,
506 );
507
508 assert_eq!(component.context_id, TEST_CONTEXT_ID);
510 assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
511
512 assert_eq!(
515 component.request(0, *FAKE_NOW).unwrap(),
516 component.context_id
517 );
518 assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
519 assert!(!*delete_called.lock().unwrap());
520 });
521 }
522
523 #[test]
524 fn test_request_with_rotation() {
525 with_test_mars(|mars, delete_called| {
526 let spy = SpyCallback::new();
527 let mut component = ContextIDComponentInner::new(
529 TEST_CONTEXT_ID,
530 FAKE_LONG_AGO_TS,
531 false,
532 spy.callback,
533 *FAKE_NOW,
534 mars,
535 );
536
537 assert_eq!(component.context_id, TEST_CONTEXT_ID);
539 assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
540
541 assert!(Uuid::parse_str(&component.request(2, *FAKE_NOW).unwrap()).is_ok());
546 assert_ne!(component.context_id, TEST_CONTEXT_ID);
547 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
548 assert!(*delete_called.lock().unwrap());
549
550 assert_eq!(
551 *spy.rotated_called.lock().unwrap(),
552 1,
553 "rotated() should have been called"
554 );
555 assert_eq!(
556 *spy.persist_called.lock().unwrap(),
557 1,
558 "persist() should have been called"
559 );
560 assert_eq!(
561 spy.last_rotated_id.lock().unwrap().as_deref().unwrap(),
562 TEST_CONTEXT_ID
563 );
564 });
565 }
566
567 #[test]
568 fn test_force_rotation() {
569 with_test_mars(|mars, delete_called| {
570 let spy = SpyCallback::new();
571
572 let mut component = ContextIDComponentInner::new(
573 TEST_CONTEXT_ID,
574 FAKE_LONG_AGO_TS,
575 false,
576 spy.callback,
577 *FAKE_NOW,
578 mars,
579 );
580
581 component.force_rotation(*FAKE_NOW);
582 assert!(Uuid::parse_str(&component.request(2, *FAKE_NOW).unwrap()).is_ok());
583 assert_ne!(component.context_id, TEST_CONTEXT_ID);
584 assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
585 assert!(*delete_called.lock().unwrap());
586 assert_eq!(
587 *spy.rotated_called.lock().unwrap(),
588 1,
589 "rotated() should have been called"
590 );
591 assert_eq!(
592 spy.last_rotated_id.lock().unwrap().as_deref().unwrap(),
593 TEST_CONTEXT_ID
594 );
595 });
596 }
597
598 #[test]
599 fn test_accept_braces() {
600 with_test_mars(|mars, delete_called| {
601 let wrapped_context_id = ["{", TEST_CONTEXT_ID, "}"].concat();
605 let mut component = ContextIDComponentInner::new(
606 &wrapped_context_id,
607 0,
608 false,
609 Box::new(DefaultContextIdCallback),
610 *FAKE_NOW,
611 mars,
612 );
613
614 assert_eq!(component.context_id, TEST_CONTEXT_ID);
617 assert!(Uuid::parse_str(&component.request(0, *FAKE_NOW).unwrap()).is_ok());
618 assert!(!*delete_called.lock().unwrap());
619 });
620 }
621
622 #[test]
623 fn test_persist_callback() {
624 with_test_mars(|mars, delete_called| {
625 let spy = SpyCallback::new();
626
627 let mut component = ContextIDComponentInner::new(
628 TEST_CONTEXT_ID,
629 FAKE_LONG_AGO_TS,
630 false,
631 spy.callback,
632 *FAKE_NOW,
633 mars,
634 );
635
636 component.force_rotation(*FAKE_NOW);
637
638 assert_eq!(
639 *spy.persist_called.lock().unwrap(),
640 1,
641 "persist() should have been called"
642 );
643
644 assert!(
645 Uuid::parse_str(spy.last_rotated_id.lock().unwrap().as_deref().unwrap()).is_ok(),
646 "persist() should have received a valid context_id string"
647 );
648
649 assert_eq!(
650 *spy.last_persist_ts.lock().unwrap(),
651 Some(FAKE_NOW_TS),
652 "persist() should have received the expected creation date"
653 );
654 assert!(*delete_called.lock().unwrap());
655 });
656 }
657}