1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::hash::Hash;
4
5pub mod reconcile;
6pub use reconcile::{Reconcile, ReconcileErrors};
7
8pub struct LifecycleContext {
9 pub display_name: String,
10 pub metadata: serde_json::Map<String, serde_json::Value>,
11}
12
13pub trait Lifecycle {
14 type Key: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned;
15 type State;
16 type Context;
17 type Output;
18 type Error;
19
20 fn key(&self) -> Self::Key;
21
22 fn display_name(&self) -> String {
26 String::new()
27 }
28
29 fn enter(
30 self,
31 ctx: &mut Self::Context,
32 output: &mut Self::Output,
33 ) -> Result<Self::State, Self::Error>;
34
35 fn reconcile_self(
36 self,
37 state: &mut Self::State,
38 ctx: &mut Self::Context,
39 output: &mut Self::Output,
40 ) -> Result<(), Self::Error>;
41
42 fn exit(
43 state: Self::State,
44 ctx: &mut Self::Context,
45 output: &mut Self::Output,
46 ) -> Result<(), Self::Error>;
47
48 fn enhance_lifecycle_context(&self, _ctx: &mut LifecycleContext) {}
49
50 fn enhance_lifecycle_state_context(_state: &Self::State, _ctx: &mut LifecycleContext) {}
51
52 fn lifecycle_context(&self) -> LifecycleContext {
53 let mut ctx = LifecycleContext {
54 display_name: self.display_name(),
55 metadata: serde_json::Map::new(),
56 };
57 self.enhance_lifecycle_context(&mut ctx);
58 ctx
59 }
60
61 fn lifecycle_state_context(state: &Self::State) -> LifecycleContext {
62 let mut ctx = LifecycleContext {
63 display_name: String::new(),
64 metadata: serde_json::Map::new(),
65 };
66 Self::enhance_lifecycle_state_context(state, &mut ctx);
67 ctx
68 }
69
70 fn wrap_enter(
71 self,
72 ctx: &mut Self::Context,
73 output: &mut Self::Output,
74 ) -> Result<Self::State, Self::Error>
75 where
76 Self: Sized,
77 {
78 self.enter(ctx, output)
79 }
80
81 fn wrap_reconcile(
82 self,
83 state: &mut Self::State,
84 ctx: &mut Self::Context,
85 output: &mut Self::Output,
86 ) -> Result<(), Self::Error>
87 where
88 Self: Sized,
89 {
90 self.reconcile_self(state, ctx, output)
91 }
92
93 fn wrap_exit(
94 state: Self::State,
95 ctx: &mut Self::Context,
96 output: &mut Self::Output,
97 ) -> Result<(), Self::Error> {
98 Self::exit(state, ctx, output)
99 }
100}
101
102pub struct OptativeSet<T: Lifecycle> {
103 store: HashMap<T::Key, T::State>,
104}
105
106impl<T: Lifecycle> Default for OptativeSet<T> {
107 fn default() -> Self {
108 Self {
109 store: HashMap::new(),
110 }
111 }
112}
113
114impl<T: Lifecycle + 'static> OptativeSet<T>
115where
116 T::Error: Debug,
117{
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn with_initial_state(items: impl IntoIterator<Item = (T::Key, T::State)>) -> Self {
126 Self {
127 store: items.into_iter().collect(),
128 }
129 }
130
131 fn dedup_by_key(items: impl IntoIterator<Item = T>) -> HashMap<T::Key, T> {
132 let mut map = HashMap::new();
133 for item in items {
134 map.insert(item.key(), item);
135 }
136 map
137 }
138
139 fn exit_removed(
140 &mut self,
141 new_map: &HashMap<T::Key, T>,
142 ctx: &mut T::Context,
143 output: &mut T::Output,
144 errors: &mut ReconcileErrors<T::Key, T::Error>,
145 ) {
146 let exit_keys: Vec<T::Key> = self
147 .store
148 .keys()
149 .filter(|k| !new_map.contains_key(*k))
150 .cloned()
151 .collect();
152 for key in exit_keys {
153 let state = self.store.remove(&key).unwrap();
154 if let Err(e) = T::wrap_exit(state, ctx, output) {
155 errors.push((key, e));
156 }
157 }
158 }
159
160 fn update_existing(
161 &mut self,
162 new_map: &mut HashMap<T::Key, T>,
163 ctx: &mut T::Context,
164 output: &mut T::Output,
165 errors: &mut ReconcileErrors<T::Key, T::Error>,
166 ) {
167 let update_keys: Vec<T::Key> = new_map
168 .keys()
169 .filter(|k| self.store.contains_key(*k))
170 .cloned()
171 .collect();
172 for key in update_keys {
173 let item = new_map.remove(&key).unwrap();
174 let state = self.store.get_mut(&key).unwrap();
175 if let Err(e) = item.wrap_reconcile(state, ctx, output) {
176 let old_state = self.store.remove(&key).unwrap();
177 if let Err(exit_e) = T::wrap_exit(old_state, ctx, output) {
178 errors.push((key.clone(), exit_e));
179 }
180 errors.push((key, e));
181 }
182 }
183 }
184
185 fn enter_new(
186 &mut self,
187 mut new_map: HashMap<T::Key, T>,
188 ctx: &mut T::Context,
189 output: &mut T::Output,
190 errors: &mut ReconcileErrors<T::Key, T::Error>,
191 ) {
192 let enter_keys: Vec<T::Key> = new_map
193 .keys()
194 .filter(|k| !self.store.contains_key(*k))
195 .cloned()
196 .collect();
197 for key in enter_keys {
198 let item = new_map.remove(&key).unwrap();
199 match item.wrap_enter(ctx, output) {
200 Ok(state) => {
201 self.store.insert(key, state);
202 }
203 Err(e) => {
204 errors.push((key, e));
205 }
206 }
207 }
208 }
209
210 pub fn get(&self, key: &T::Key) -> Option<&T::State> {
211 self.store.get(key)
212 }
213
214 pub fn iter(&self) -> impl Iterator<Item = (&T::Key, &T::State)> {
215 self.store.iter()
216 }
217
218 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&T::Key, &mut T::State)> {
219 self.store.iter_mut()
220 }
221
222 pub fn get_mut(&mut self, key: &T::Key) -> Option<&mut T::State> {
223 self.store.get_mut(key)
224 }
225}
226
227impl<T: Lifecycle + 'static> reconcile::Reconcile<T> for OptativeSet<T>
228where
229 T::Error: Debug,
230{
231 fn reconcile(
232 &mut self,
233 desired: impl IntoIterator<Item = T>,
234 ctx: &mut T::Context,
235 output: &mut T::Output,
236 ) -> ReconcileErrors<T::Key, T::Error> {
237 let mut errors = ReconcileErrors::new();
238 let mut new_map = Self::dedup_by_key(desired);
239 self.exit_removed(&new_map, ctx, output, &mut errors);
240 self.update_existing(&mut new_map, ctx, output, &mut errors);
241 self.enter_new(new_map, ctx, output, &mut errors);
242 errors
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use std::sync::{Arc, Mutex};
250
251 #[derive(Clone)]
252 struct TestSpec {
253 id: String,
254 value: i32,
255 }
256
257 impl std::fmt::Display for TestSpec {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 write!(f, "{}", self.id)
260 }
261 }
262
263 impl Lifecycle for TestSpec {
264 type Key = String;
265 type State = i32;
266 type Context = Arc<Mutex<Vec<String>>>;
267 type Output = ();
268 type Error = std::convert::Infallible;
269
270 fn key(&self) -> String {
271 self.id.clone()
272 }
273
274 fn enter(
275 self,
276 ctx: &mut Self::Context,
277 _output: &mut (),
278 ) -> Result<Self::State, Self::Error> {
279 ctx.lock().unwrap().push(format!("enter:{}", self.id));
280 Ok(self.value)
281 }
282
283 fn reconcile_self(
284 self,
285 state: &mut Self::State,
286 ctx: &mut Self::Context,
287 _output: &mut (),
288 ) -> Result<(), Self::Error> {
289 ctx.lock()
290 .unwrap()
291 .push(format!("reconcile_self:{}", self.id));
292 *state = self.value;
293 Ok(())
294 }
295
296 fn exit(
297 state: Self::State,
298 ctx: &mut Self::Context,
299 _output: &mut Self::Output,
300 ) -> Result<(), Self::Error> {
301 ctx.lock().unwrap().push(format!("exit:{}", state));
302 Ok(())
303 }
304 }
305
306 fn make_ctx() -> Arc<Mutex<Vec<String>>> {
307 Arc::new(Mutex::new(Vec::new()))
308 }
309
310 fn calls(ctx: &Arc<Mutex<Vec<String>>>) -> Vec<String> {
311 ctx.lock().unwrap().clone()
312 }
313
314 #[test]
315 fn new_item_calls_enter_and_stores_state() {
316 let mut ctx = make_ctx();
317 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
318 ms.reconcile(
319 vec![TestSpec {
320 id: "a".to_string(),
321 value: 42,
322 }],
323 &mut ctx,
324 &mut (),
325 );
326 assert!(calls(&ctx).contains(&"enter:a".to_string()));
327 assert_eq!(ms.get(&"a".to_string()), Some(&42));
328 }
329
330 #[test]
331 fn removed_item_calls_exit_with_old_state() {
332 let mut ctx = make_ctx();
333 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
334 ms.reconcile(
335 vec![TestSpec {
336 id: "a".to_string(),
337 value: 99,
338 }],
339 &mut ctx,
340 &mut (),
341 );
342 ms.reconcile(vec![], &mut ctx, &mut ());
343 assert!(calls(&ctx).contains(&"exit:99".to_string()));
344 }
345
346 #[test]
347 fn existing_item_calls_reconcile_self_not_enter() {
348 let mut ctx = make_ctx();
349 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
350 ms.reconcile(
351 vec![TestSpec {
352 id: "a".to_string(),
353 value: 1,
354 }],
355 &mut ctx,
356 &mut (),
357 );
358 ms.reconcile(
359 vec![TestSpec {
360 id: "a".to_string(),
361 value: 2,
362 }],
363 &mut ctx,
364 &mut (),
365 );
366 let log = calls(&ctx);
367 assert_eq!(log.iter().filter(|c| *c == "enter:a").count(), 1);
368 assert!(log.contains(&"reconcile_self:a".to_string()));
369 }
370
371 #[test]
372 fn duplicate_keys_in_batch_only_one_enter() {
373 let mut ctx = make_ctx();
374 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
375 ms.reconcile(
376 vec![
377 TestSpec {
378 id: "a".to_string(),
379 value: 1,
380 },
381 TestSpec {
382 id: "a".to_string(),
383 value: 2,
384 },
385 ],
386 &mut ctx,
387 &mut (),
388 );
389 let log = calls(&ctx);
390 assert_eq!(log.iter().filter(|c| *c == "enter:a").count(), 1);
391 }
392
393 #[test]
394 fn get_returns_state_after_enter() {
395 let mut ctx = make_ctx();
396 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
397 ms.reconcile(
398 vec![TestSpec {
399 id: "b".to_string(),
400 value: 7,
401 }],
402 &mut ctx,
403 &mut (),
404 );
405 assert_eq!(ms.get(&"b".to_string()), Some(&7));
406 }
407
408 #[test]
409 fn get_returns_updated_state_after_reconcile() {
410 let mut ctx = make_ctx();
411 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
412 ms.reconcile(
413 vec![TestSpec {
414 id: "c".to_string(),
415 value: 10,
416 }],
417 &mut ctx,
418 &mut (),
419 );
420 ms.reconcile(
421 vec![TestSpec {
422 id: "c".to_string(),
423 value: 20,
424 }],
425 &mut ctx,
426 &mut (),
427 );
428 assert_eq!(ms.get(&"c".to_string()), Some(&20));
429 }
430
431 #[test]
432 fn iter_mut_yields_mutable_state_visible_via_get() {
433 let mut ctx = make_ctx();
434 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
435 ms.reconcile(
436 vec![TestSpec {
437 id: "d".to_string(),
438 value: 5,
439 }],
440 &mut ctx,
441 &mut (),
442 );
443 for (_k, v) in ms.iter_mut() {
444 *v = 99;
445 }
446 assert_eq!(ms.get(&"d".to_string()), Some(&99));
447 }
448
449 #[test]
450 fn get_mut_returns_mutable_reference_visible_via_get() {
451 let mut ctx = make_ctx();
452 let mut ms: OptativeSet<TestSpec> = OptativeSet::new();
453 ms.reconcile(
454 vec![TestSpec {
455 id: "e".to_string(),
456 value: 3,
457 }],
458 &mut ctx,
459 &mut (),
460 );
461 if let Some(v) = ms.get_mut(&"e".to_string()) {
462 *v = 77;
463 }
464 assert_eq!(ms.get(&"e".to_string()), Some(&77));
465 }
466
467 mod enter_err {
468 use super::super::*;
469
470 #[derive(Clone)]
471 struct FallibleSpec {
472 id: String,
473 fail: bool,
474 }
475
476 impl std::fmt::Display for FallibleSpec {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 write!(f, "{}", self.id)
479 }
480 }
481
482 #[derive(Debug, PartialEq)]
483 struct FallibleError(String);
484
485 impl Lifecycle for FallibleSpec {
486 type Key = String;
487 type State = String;
488 type Context = ();
489 type Output = ();
490 type Error = FallibleError;
491
492 fn key(&self) -> String {
493 self.id.clone()
494 }
495
496 fn enter(self, _ctx: &mut (), _output: &mut ()) -> Result<String, FallibleError> {
497 if self.fail {
498 Err(FallibleError(format!("enter failed for {}", self.id)))
499 } else {
500 Ok(format!("state:{}", self.id))
501 }
502 }
503
504 fn reconcile_self(
505 self,
506 state: &mut String,
507 _ctx: &mut (),
508 _output: &mut (),
509 ) -> Result<(), FallibleError> {
510 *state = format!("updated:{}", self.id);
511 Ok(())
512 }
513
514 fn exit(
515 _state: String,
516 _ctx: &mut (),
517 _output: &mut Self::Output,
518 ) -> Result<(), FallibleError> {
519 Ok(())
520 }
521 }
522
523 #[test]
524 fn enter_err_not_added_to_store_error_returned() {
525 let mut ms: OptativeSet<FallibleSpec> = OptativeSet::new();
526 let errors = ms.reconcile(
527 vec![FallibleSpec {
528 id: "x".to_string(),
529 fail: true,
530 }],
531 &mut (),
532 &mut (),
533 );
534 assert!(
535 ms.get(&"x".to_string()).is_none(),
536 "item must not be in store after enter Err"
537 );
538 assert_eq!(errors.len(), 1, "one error must be returned");
539 assert_eq!(errors[0].0, "x");
540 assert_eq!(errors[0].1, FallibleError("enter failed for x".to_string()));
541 }
542
543 #[test]
544 fn enter_ok_adds_item_to_store_no_errors() {
545 let mut ms: OptativeSet<FallibleSpec> = OptativeSet::new();
546 let errors = ms.reconcile(
547 vec![FallibleSpec {
548 id: "y".to_string(),
549 fail: false,
550 }],
551 &mut (),
552 &mut (),
553 );
554 assert_eq!(ms.get(&"y".to_string()), Some(&"state:y".to_string()));
555 assert!(errors.is_empty(), "no errors when enter returns Ok");
556 }
557 }
558
559 mod reconcile_err {
560 use super::super::*;
561
562 #[derive(Clone)]
563 struct UpdateFallibleSpec {
564 id: String,
565 fail_update: bool,
566 }
567
568 impl std::fmt::Display for UpdateFallibleSpec {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 write!(f, "{}", self.id)
571 }
572 }
573
574 #[derive(Debug, PartialEq)]
575 struct UpdateError(String);
576
577 static EXIT_CALLED: std::sync::atomic::AtomicBool =
578 std::sync::atomic::AtomicBool::new(false);
579
580 impl Lifecycle for UpdateFallibleSpec {
581 type Key = String;
582 type State = String;
583 type Context = ();
584 type Output = ();
585 type Error = UpdateError;
586
587 fn key(&self) -> String {
588 self.id.clone()
589 }
590
591 fn enter(self, _ctx: &mut (), _output: &mut ()) -> Result<String, UpdateError> {
592 Ok(format!("state:{}", self.id))
593 }
594
595 fn reconcile_self(
596 self,
597 _state: &mut String,
598 _ctx: &mut (),
599 _output: &mut (),
600 ) -> Result<(), UpdateError> {
601 if self.fail_update {
602 Err(UpdateError(format!("update failed for {}", self.id)))
603 } else {
604 Ok(())
605 }
606 }
607
608 fn exit(
609 _state: String,
610 _ctx: &mut (),
611 _output: &mut Self::Output,
612 ) -> Result<(), UpdateError> {
613 EXIT_CALLED.store(true, std::sync::atomic::Ordering::SeqCst);
614 Ok(())
615 }
616 }
617
618 #[test]
619 fn reconcile_err_exit_called_entry_removed_error_returned() {
620 EXIT_CALLED.store(false, std::sync::atomic::Ordering::SeqCst);
621 let mut ms: OptativeSet<UpdateFallibleSpec> = OptativeSet::new();
622
623 let e1 = ms.reconcile(
624 vec![UpdateFallibleSpec {
625 id: "z".to_string(),
626 fail_update: false,
627 }],
628 &mut (),
629 &mut (),
630 );
631 assert!(e1.is_empty());
632 assert!(ms.get(&"z".to_string()).is_some());
633
634 let errors = ms.reconcile(
635 vec![UpdateFallibleSpec {
636 id: "z".to_string(),
637 fail_update: true,
638 }],
639 &mut (),
640 &mut (),
641 );
642 assert_eq!(errors.len(), 1);
643 assert_eq!(errors[0].0, "z");
644 assert_eq!(errors[0].1, UpdateError("update failed for z".to_string()));
645 assert!(ms.get(&"z".to_string()).is_none());
646 assert!(EXIT_CALLED.load(std::sync::atomic::Ordering::SeqCst));
647
648 EXIT_CALLED.store(false, std::sync::atomic::Ordering::SeqCst);
649 let e3 = ms.reconcile(
650 vec![UpdateFallibleSpec {
651 id: "z".to_string(),
652 fail_update: false,
653 }],
654 &mut (),
655 &mut (),
656 );
657 assert!(e3.is_empty());
658 assert!(ms.get(&"z".to_string()).is_some());
659 }
660 }
661
662 mod channel_output {
663 use super::super::*;
664 use std::sync::mpsc;
665
666 #[derive(Clone)]
667 struct ChannelOutputLifecycle {
668 id: String,
669 }
670
671 impl std::fmt::Display for ChannelOutputLifecycle {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 write!(f, "{}", self.id)
674 }
675 }
676
677 impl Lifecycle for ChannelOutputLifecycle {
678 type Key = String;
679 type State = ();
680 type Context = ();
681 type Output = mpsc::Sender<String>;
682 type Error = std::convert::Infallible;
683
684 fn key(&self) -> String {
685 self.id.clone()
686 }
687
688 fn enter(
689 self,
690 _ctx: &mut (),
691 output: &mut mpsc::Sender<String>,
692 ) -> Result<(), Self::Error> {
693 output.send(format!("entered:{}", self.id)).unwrap();
694 Ok(())
695 }
696
697 fn reconcile_self(
698 self,
699 _state: &mut (),
700 _ctx: &mut (),
701 output: &mut mpsc::Sender<String>,
702 ) -> Result<(), Self::Error> {
703 output.send(format!("reconciled:{}", self.id)).unwrap();
704 Ok(())
705 }
706
707 fn exit(
708 _state: (),
709 _ctx: &mut (),
710 _output: &mut Self::Output,
711 ) -> Result<(), Self::Error> {
712 Ok(())
713 }
714 }
715
716 #[test]
717 fn enter_receives_output_and_can_write_to_it() {
718 let (mut tx, rx) = mpsc::channel::<String>();
719 let mut ms: OptativeSet<ChannelOutputLifecycle> = OptativeSet::new();
720 ms.reconcile(
721 vec![ChannelOutputLifecycle {
722 id: "o1".to_string(),
723 }],
724 &mut (),
725 &mut tx,
726 );
727 drop(tx);
728 let msgs: Vec<String> = rx.try_iter().collect();
729 assert!(msgs.contains(&"entered:o1".to_string()));
730 }
731
732 #[test]
733 fn exit_does_not_receive_output() {
734 let (mut tx, rx) = mpsc::channel::<String>();
735 let mut ms: OptativeSet<ChannelOutputLifecycle> = OptativeSet::new();
736 ms.reconcile(
737 vec![ChannelOutputLifecycle {
738 id: "o2".to_string(),
739 }],
740 &mut (),
741 &mut tx,
742 );
743 ms.reconcile(vec![], &mut (), &mut tx);
744 drop(tx);
745 let msgs: Vec<String> = rx.try_iter().collect();
746 assert!(!msgs.iter().any(|m| m.starts_with("exited:")));
747 }
748 }
749}