1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
51
52pub type ParticipantId = String;
54
55pub type MessageType = String;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Action {
61 Emit(MessageType),
63 Handle(MessageType, String),
65 Local(String),
67}
68
69#[derive(Debug, Clone)]
71pub struct ChoreographyStep {
72 pub participant: ParticipantId,
74 pub action: Action,
76 pub sequence: usize,
78}
79
80#[derive(Debug, Clone)]
82pub struct Choreography {
83 pub name: String,
85 steps: Vec<ChoreographyStep>,
87}
88
89impl Choreography {
90 pub fn new(name: impl Into<String>) -> Self {
92 Self {
93 name: name.into(),
94 steps: Vec::new(),
95 }
96 }
97
98 pub fn step(&mut self, participant: impl Into<String>, action: Action) -> &mut Self {
100 let seq = self.steps.len();
101 self.steps.push(ChoreographyStep {
102 participant: participant.into(),
103 action,
104 sequence: seq,
105 });
106 self
107 }
108
109 pub fn steps(&self) -> &[ChoreographyStep] {
111 &self.steps
112 }
113
114 pub fn participants(&self) -> BTreeSet<ParticipantId> {
116 self.steps.iter().map(|s| s.participant.clone()).collect()
117 }
118
119 pub fn message_types(&self) -> BTreeSet<MessageType> {
121 let mut types = BTreeSet::new();
122 for step in &self.steps {
123 match &step.action {
124 Action::Emit(msg) => {
125 types.insert(msg.clone());
126 }
127 Action::Handle(msg, _) => {
128 types.insert(msg.clone());
129 }
130 Action::Local(_) => {}
131 }
132 }
133 types
134 }
135
136 pub fn project(&self) -> Projection {
138 let mut handlers: BTreeMap<ParticipantId, Vec<ProjectedHandler>> = BTreeMap::new();
139
140 for step in &self.steps {
141 let handler = match &step.action {
142 Action::Emit(msg) => ProjectedHandler {
143 kind: HandlerKind::Emit,
144 message: msg.clone(),
145 handler_name: format!("emit_{msg}"),
146 sequence: step.sequence,
147 },
148 Action::Handle(msg, name) => ProjectedHandler {
149 kind: HandlerKind::Receive,
150 message: msg.clone(),
151 handler_name: name.clone(),
152 sequence: step.sequence,
153 },
154 Action::Local(name) => ProjectedHandler {
155 kind: HandlerKind::Local,
156 message: String::new(),
157 handler_name: name.clone(),
158 sequence: step.sequence,
159 },
160 };
161 handlers
162 .entry(step.participant.clone())
163 .or_default()
164 .push(handler);
165 }
166
167 Projection { handlers }
168 }
169
170 pub fn verify_deadlock_free(&self) -> bool {
177 let mut emitters: HashMap<&str, Vec<usize>> = HashMap::new();
178 let mut receivers: HashMap<&str, Vec<usize>> = HashMap::new();
179
180 for (i, step) in self.steps.iter().enumerate() {
181 match &step.action {
182 Action::Emit(msg) => emitters.entry(msg.as_str()).or_default().push(i),
183 Action::Handle(msg, _) => receivers.entry(msg.as_str()).or_default().push(i),
184 Action::Local(_) => {}
185 }
186 }
187
188 for msg in emitters.keys() {
190 if !receivers.contains_key(msg) {
191 return false;
192 }
193 }
194
195 for msg in receivers.keys() {
197 if !emitters.contains_key(msg) {
198 return false;
199 }
200 }
201
202 for (msg, emit_indices) in &emitters {
204 if let Some(recv_indices) = receivers.get(msg) {
205 let min_emit = emit_indices.iter().min().copied().unwrap_or(usize::MAX);
206 let min_recv = recv_indices.iter().min().copied().unwrap_or(0);
207 if min_recv < min_emit {
208 return false; }
210 }
211 }
212
213 let mut dep_graph: HashMap<&str, HashSet<&str>> = HashMap::new();
216 for step in &self.steps {
217 if let Action::Handle(msg, _) = &step.action {
218 for emit_step in &self.steps {
220 if let Action::Emit(emsg) = &emit_step.action
221 && emsg == msg
222 {
223 dep_graph
224 .entry(step.participant.as_str())
225 .or_default()
226 .insert(emit_step.participant.as_str());
227 }
228 }
229 }
230 }
231
232 !has_cycle(&dep_graph)
234 }
235
236 pub fn completeness_report(&self) -> CompletenessReport {
238 let mut emitted: BTreeSet<String> = BTreeSet::new();
239 let mut handled: BTreeSet<String> = BTreeSet::new();
240
241 for step in &self.steps {
242 match &step.action {
243 Action::Emit(msg) => {
244 emitted.insert(msg.clone());
245 }
246 Action::Handle(msg, _) => {
247 handled.insert(msg.clone());
248 }
249 Action::Local(_) => {}
250 }
251 }
252
253 let unhandled: Vec<String> = emitted.difference(&handled).cloned().collect();
254 let orphaned: Vec<String> = handled.difference(&emitted).cloned().collect();
255
256 CompletenessReport {
257 is_complete: unhandled.is_empty() && orphaned.is_empty(),
258 unhandled_messages: unhandled,
259 orphaned_handlers: orphaned,
260 participant_count: self.participants().len(),
261 message_count: self.message_types().len(),
262 step_count: self.steps.len(),
263 }
264 }
265}
266
267fn has_cycle<'a>(graph: &HashMap<&'a str, HashSet<&'a str>>) -> bool {
269 let mut visited = HashSet::new();
270 let mut in_stack = HashSet::new();
271
272 for &node in graph.keys() {
273 if !visited.contains(node) && dfs_cycle(node, graph, &mut visited, &mut in_stack) {
274 return true;
275 }
276 }
277 false
278}
279
280fn dfs_cycle<'a>(
281 node: &'a str,
282 graph: &HashMap<&'a str, HashSet<&'a str>>,
283 visited: &mut HashSet<&'a str>,
284 in_stack: &mut HashSet<&'a str>,
285) -> bool {
286 visited.insert(node);
287 in_stack.insert(node);
288
289 if let Some(neighbors) = graph.get(node) {
290 for &next in neighbors {
291 if !visited.contains(next) {
292 if dfs_cycle(next, graph, visited, in_stack) {
293 return true;
294 }
295 } else if in_stack.contains(next) {
296 return true;
297 }
298 }
299 }
300
301 in_stack.remove(node);
302 false
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct ProjectedHandler {
308 pub kind: HandlerKind,
310 pub message: String,
312 pub handler_name: String,
314 pub sequence: usize,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum HandlerKind {
321 Emit,
323 Receive,
325 Local,
327}
328
329#[derive(Debug, Clone)]
331pub struct Projection {
332 handlers: BTreeMap<ParticipantId, Vec<ProjectedHandler>>,
333}
334
335impl Projection {
336 pub fn handlers(&self, participant: &str) -> &[ProjectedHandler] {
338 self.handlers
339 .get(participant)
340 .map(|v| v.as_slice())
341 .unwrap_or(&[])
342 }
343
344 pub fn participants(&self) -> Vec<&str> {
346 self.handlers.keys().map(|s| s.as_str()).collect()
347 }
348
349 pub fn is_complete(&self) -> bool {
351 let mut emitted: HashSet<&str> = HashSet::new();
352 let mut received: HashSet<&str> = HashSet::new();
353
354 for handlers in self.handlers.values() {
355 for h in handlers {
356 match h.kind {
357 HandlerKind::Emit => {
358 emitted.insert(h.message.as_str());
359 }
360 HandlerKind::Receive => {
361 received.insert(h.message.as_str());
362 }
363 HandlerKind::Local => {}
364 }
365 }
366 }
367
368 emitted == received
369 }
370}
371
372#[derive(Debug, Clone)]
374pub struct CompletenessReport {
375 pub is_complete: bool,
377 pub unhandled_messages: Vec<String>,
379 pub orphaned_handlers: Vec<String>,
381 pub participant_count: usize,
383 pub message_count: usize,
385 pub step_count: usize,
387}
388
389pub fn execute_choreography(choreo: &Choreography) -> Vec<ExecutionEvent> {
393 let mut events = Vec::new();
394 let mut pending_messages: VecDeque<(String, usize)> = VecDeque::new();
395
396 for step in choreo.steps() {
397 match &step.action {
398 Action::Emit(msg) => {
399 pending_messages.push_back((msg.clone(), step.sequence));
400 events.push(ExecutionEvent {
401 sequence: step.sequence,
402 participant: step.participant.clone(),
403 kind: EventKind::Emit(msg.clone()),
404 });
405 }
406 Action::Handle(msg, handler) => {
407 let delivered = pending_messages.iter().any(|(m, _)| m == msg);
409 events.push(ExecutionEvent {
410 sequence: step.sequence,
411 participant: step.participant.clone(),
412 kind: if delivered {
413 EventKind::Deliver(msg.clone(), handler.clone())
414 } else {
415 EventKind::Deadlock(msg.clone())
416 },
417 });
418 }
419 Action::Local(name) => {
420 events.push(ExecutionEvent {
421 sequence: step.sequence,
422 participant: step.participant.clone(),
423 kind: EventKind::Local(name.clone()),
424 });
425 }
426 }
427 }
428
429 events
430}
431
432#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct ExecutionEvent {
435 pub sequence: usize,
437 pub participant: ParticipantId,
439 pub kind: EventKind,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445pub enum EventKind {
446 Emit(String),
448 Deliver(String, String),
450 Local(String),
452 Deadlock(String),
454}
455
456pub fn example_filter_update() -> Choreography {
462 let mut c = Choreography::new("filter_update");
463 c.step("filter", Action::Emit("filter_changed".into()));
464 c.step(
465 "list",
466 Action::Handle("filter_changed".into(), "scroll_to_top".into()),
467 );
468 c.step(
469 "status",
470 Action::Handle("filter_changed".into(), "update_count".into()),
471 );
472 c.step(
473 "header",
474 Action::Handle("filter_changed".into(), "highlight_filter".into()),
475 );
476 c
477}
478
479pub fn example_selection_sync() -> Choreography {
481 let mut c = Choreography::new("selection_sync");
482 c.step("list", Action::Emit("selection_changed".into()));
483 c.step(
484 "detail",
485 Action::Handle("selection_changed".into(), "show_details".into()),
486 );
487 c.step(
488 "status",
489 Action::Handle("selection_changed".into(), "update_selection_info".into()),
490 );
491 c
492}
493
494pub fn example_tab_navigation() -> Choreography {
496 let mut c = Choreography::new("tab_navigation");
497 c.step("tabs", Action::Emit("tab_changed".into()));
498 c.step(
499 "content",
500 Action::Handle("tab_changed".into(), "switch_view".into()),
501 );
502 c.step(
503 "breadcrumb",
504 Action::Handle("tab_changed".into(), "update_path".into()),
505 );
506 c.step(
507 "status",
508 Action::Handle("tab_changed".into(), "update_tab_info".into()),
509 );
510 c
511}
512
513pub fn example_search_flow() -> Choreography {
515 let mut c = Choreography::new("search_flow");
516 c.step("search_input", Action::Emit("query_changed".into()));
517 c.step("search_input", Action::Local("debounce".into()));
518 c.step(
519 "results",
520 Action::Handle("query_changed".into(), "filter_results".into()),
521 );
522 c.step("results", Action::Emit("results_updated".into()));
523 c.step(
524 "status",
525 Action::Handle("results_updated".into(), "update_match_count".into()),
526 );
527 c.step(
528 "preview",
529 Action::Handle("results_updated".into(), "update_preview".into()),
530 );
531 c
532}
533
534pub fn example_modal_dialog() -> Choreography {
536 let mut c = Choreography::new("modal_dialog");
537 c.step("trigger", Action::Emit("open_modal".into()));
538 c.step(
539 "overlay",
540 Action::Handle("open_modal".into(), "show_overlay".into()),
541 );
542 c.step(
543 "dimmer",
544 Action::Handle("open_modal".into(), "dim_background".into()),
545 );
546 c.step(
547 "focus_trap",
548 Action::Handle("open_modal".into(), "capture_focus".into()),
549 );
550 c
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
558 fn filter_update_is_deadlock_free() {
559 let c = example_filter_update();
560 assert!(c.verify_deadlock_free());
561 }
562
563 #[test]
564 fn filter_update_projection() {
565 let c = example_filter_update();
566 let p = c.project();
567 assert_eq!(p.participants().len(), 4);
568 assert_eq!(p.handlers("filter").len(), 1);
569 assert_eq!(p.handlers("list").len(), 1);
570 assert_eq!(p.handlers("status").len(), 1);
571 assert_eq!(p.handlers("header").len(), 1);
572 assert!(p.is_complete());
573 }
574
575 #[test]
576 fn filter_update_completeness() {
577 let c = example_filter_update();
578 let report = c.completeness_report();
579 assert!(report.is_complete);
580 assert_eq!(report.participant_count, 4);
581 assert_eq!(report.message_count, 1);
582 assert_eq!(report.step_count, 4);
583 }
584
585 #[test]
586 fn selection_sync_works() {
587 let c = example_selection_sync();
588 assert!(c.verify_deadlock_free());
589 let p = c.project();
590 assert!(p.is_complete());
591 assert_eq!(p.participants().len(), 3);
592 }
593
594 #[test]
595 fn tab_navigation_works() {
596 let c = example_tab_navigation();
597 assert!(c.verify_deadlock_free());
598 assert!(c.project().is_complete());
599 }
600
601 #[test]
602 fn search_flow_chained_messages() {
603 let c = example_search_flow();
604 assert!(c.verify_deadlock_free());
605 let p = c.project();
606 assert!(p.is_complete());
607 assert_eq!(c.message_types().len(), 2); }
609
610 #[test]
611 fn modal_dialog_works() {
612 let c = example_modal_dialog();
613 assert!(c.verify_deadlock_free());
614 assert!(c.project().is_complete());
615 }
616
617 #[test]
618 fn orphaned_handler_detected() {
619 let mut c = Choreography::new("broken");
620 c.step(
621 "widget_a",
622 Action::Handle("nonexistent".into(), "do_stuff".into()),
623 );
624 let report = c.completeness_report();
625 assert!(!report.is_complete);
626 assert_eq!(report.orphaned_handlers, vec!["nonexistent"]);
627 }
628
629 #[test]
630 fn unhandled_message_detected() {
631 let mut c = Choreography::new("incomplete");
632 c.step("widget_a", Action::Emit("fire_and_forget".into()));
633 let report = c.completeness_report();
634 assert!(!report.is_complete);
635 assert_eq!(report.unhandled_messages, vec!["fire_and_forget"]);
636 }
637
638 #[test]
639 fn deadlock_from_missing_emitter() {
640 let mut c = Choreography::new("deadlock");
641 c.step(
642 "widget_a",
643 Action::Handle("missing".into(), "handler".into()),
644 );
645 assert!(!c.verify_deadlock_free());
646 }
647
648 #[test]
649 fn deadlock_from_handle_before_emit() {
650 let mut c = Choreography::new("out_of_order");
651 c.step("widget_b", Action::Handle("msg".into(), "process".into()));
652 c.step("widget_a", Action::Emit("msg".into()));
653 assert!(!c.verify_deadlock_free());
654 }
655
656 #[test]
657 fn circular_dependency_detected() {
658 let mut c = Choreography::new("circular");
659 c.step("a", Action::Emit("msg_ab".into()));
660 c.step("b", Action::Handle("msg_ab".into(), "handle_ab".into()));
661 c.step("b", Action::Emit("msg_ba".into()));
662 c.step("a", Action::Handle("msg_ba".into(), "handle_ba".into()));
663 assert!(!c.verify_deadlock_free());
664 }
665
666 #[test]
667 fn execute_filter_update() {
668 let c = example_filter_update();
669 let events = execute_choreography(&c);
670 assert_eq!(events.len(), 4);
671 assert!(matches!(&events[0].kind, EventKind::Emit(m) if m == "filter_changed"));
672 assert!(matches!(&events[1].kind, EventKind::Deliver(m, _) if m == "filter_changed"));
673 assert!(matches!(&events[2].kind, EventKind::Deliver(m, _) if m == "filter_changed"));
674 assert!(matches!(&events[3].kind, EventKind::Deliver(m, _) if m == "filter_changed"));
675 }
676
677 #[test]
678 fn execute_detects_deadlock_event() {
679 let mut c = Choreography::new("deadlock_exec");
680 c.step(
681 "widget_b",
682 Action::Handle("missing".into(), "handler".into()),
683 );
684 let events = execute_choreography(&c);
685 assert_eq!(events.len(), 1);
686 assert!(matches!(&events[0].kind, EventKind::Deadlock(m) if m == "missing"));
687 }
688
689 #[test]
690 fn empty_choreography() {
691 let c = Choreography::new("empty");
692 assert!(c.verify_deadlock_free());
693 assert!(c.project().is_complete());
694 let report = c.completeness_report();
695 assert!(report.is_complete);
696 assert_eq!(report.step_count, 0);
697 }
698
699 #[test]
700 fn local_actions_preserved() {
701 let mut c = Choreography::new("with_local");
702 c.step("widget", Action::Local("compute".into()));
703 let p = c.project();
704 assert_eq!(p.handlers("widget").len(), 1);
705 assert_eq!(p.handlers("widget")[0].kind, HandlerKind::Local);
706 }
707
708 #[test]
709 fn choreography_determinism() {
710 let c1 = example_search_flow();
711 let c2 = example_search_flow();
712 let p1 = c1.project();
713 let p2 = c2.project();
714 assert_eq!(p1.participants(), p2.participants());
715 for part in p1.participants() {
716 assert_eq!(p1.handlers(part), p2.handlers(part));
717 }
718 }
719
720 #[test]
721 fn comparison_manual_vs_choreographic() {
722 let c = example_filter_update();
725 let p = c.project();
726
727 assert_eq!(p.handlers("filter")[0].kind, HandlerKind::Emit);
730 assert_eq!(p.handlers("list")[0].kind, HandlerKind::Receive);
731 assert_eq!(p.handlers("status")[0].kind, HandlerKind::Receive);
732 assert_eq!(p.handlers("header")[0].kind, HandlerKind::Receive);
733
734 assert!(c.completeness_report().is_complete);
736 }
737}