1pub mod query;
13mod window;
14
15pub use window::{
16 FactSource, Frame, Lifecycle, Presentation, SizeState, Visibility, WindowAction, WindowFacts,
17 WindowInfo,
18};
19
20use bern_protocol::{CommandError, SessionCommand, Type, TypeTable, Value};
21use std::collections::BTreeMap;
22use std::sync::{Arc, Mutex};
23
24#[derive(Clone, Debug)]
26pub struct SessionType {
27 pub name: String,
28 pub fields: Vec<(String, Type, Value)>,
30}
31
32#[derive(Clone, Debug)]
34pub struct WindowKind {
35 pub id: String,
36 pub sessions: Vec<usize>,
38 pub presentation: Presentation,
39}
40
41#[derive(Clone, Debug)]
43pub struct SessionSpec {
44 pub types: TypeTable,
45 pub sessions: Vec<SessionType>,
46 pub windows: Vec<WindowKind>,
47}
48
49#[derive(Clone, Debug, PartialEq)]
50pub struct WindowInstance {
51 pub kind: usize,
52 pub generation: u64,
53 pub id: String,
55 pub drafts: Vec<Vec<Value>>,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum CloseDecision {
62 Save,
64 Discard,
66 Cancel,
68}
69
70impl CloseDecision {
71 pub const ALL: [CloseDecision; 3] = [
72 CloseDecision::Save,
73 CloseDecision::Discard,
74 CloseDecision::Cancel,
75 ];
76
77 pub fn index(self) -> u32 {
78 Self::ALL
79 .iter()
80 .position(|decision| *decision == self)
81 .unwrap() as u32
82 }
83
84 pub fn from_index(index: u32) -> Option<Self> {
85 Self::ALL.get(index as usize).copied()
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct Confirmation {
93 pub id: String,
94 pub window: String,
95 pub generation: u64,
96 pub draft_revision: u64,
97 pub can_save: bool,
98}
99
100impl Confirmation {
101 pub fn decisions(&self) -> Vec<CloseDecision> {
103 CloseDecision::ALL
104 .into_iter()
105 .filter(|decision| self.can_save || *decision != CloseDecision::Save)
106 .collect()
107 }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
112pub enum CloseStep {
113 Close,
115 Confirm(Confirmation),
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum DecisionStep {
123 Close,
125 Save,
127 Cancel,
129}
130
131#[derive(Debug)]
132struct WindowState {
133 generation: u64,
134 open: bool,
135 drafts: Vec<Vec<Value>>,
136 revision: u64,
138 pending: Option<Confirmation>,
139 facts: Option<WindowFacts>,
141 fact_version: u64,
142 fact_source: Option<FactSource>,
143}
144
145#[derive(Debug)]
147pub struct UiSession {
148 session_id: String,
149 spec: Arc<SessionSpec>,
150 windows: Vec<WindowState>,
151 revision: u64,
152 confirmations: u64,
153}
154
155fn stale() -> CommandError {
156 CommandError::new(
157 "stale_window",
158 "the window instance was closed; use the current instance",
159 )
160}
161
162fn stale_confirmation(message: &str) -> CommandError {
163 CommandError::new("stale_confirmation", message)
164}
165
166impl UiSession {
167 pub fn new(session_id: impl Into<String>, spec: Arc<SessionSpec>) -> Self {
168 let windows = spec
169 .windows
170 .iter()
171 .map(|_| WindowState {
172 generation: 0,
173 open: false,
174 drafts: Vec::new(),
175 revision: 0,
176 pending: None,
177 facts: None,
178 fact_version: 0,
179 fact_source: None,
180 })
181 .collect();
182 Self {
183 session_id: session_id.into(),
184 spec,
185 windows,
186 revision: 0,
187 confirmations: 0,
188 }
189 }
190
191 pub fn session_id(&self) -> &str {
192 &self.session_id
193 }
194
195 pub fn spec(&self) -> &SessionSpec {
196 &self.spec
197 }
198
199 pub fn revision(&self) -> u64 {
201 self.revision
202 }
203
204 fn initial_drafts(&self, kind: usize) -> Vec<Vec<Value>> {
205 self.spec.windows[kind]
206 .sessions
207 .iter()
208 .map(|session| {
209 self.spec.sessions[*session]
210 .fields
211 .iter()
212 .map(|(_, _, initial)| initial.clone())
213 .collect()
214 })
215 .collect()
216 }
217
218 fn instance(&self, kind: usize) -> WindowInstance {
219 let state = &self.windows[kind];
220 WindowInstance {
221 kind,
222 generation: state.generation,
223 id: format!("{}#{}", self.spec.windows[kind].id, state.generation),
224 drafts: state.drafts.clone(),
225 }
226 }
227
228 pub fn window_opened(&mut self, kind: usize) -> Result<WindowInstance, CommandError> {
232 if kind >= self.windows.len() {
233 return Err(CommandError::new("target_not_found", "unknown window kind"));
234 }
235 if !self.windows[kind].open {
236 let drafts = self.initial_drafts(kind);
237 let state = &mut self.windows[kind];
238 state.generation += 1;
239 state.open = true;
240 state.drafts = drafts;
241 state.revision = 0;
242 state.pending = None;
243 state.facts = None;
244 state.fact_version = 0;
245 state.fact_source = None;
246 self.revision += 1;
247 }
248 Ok(self.instance(kind))
249 }
250
251 pub fn window_facts(
255 &mut self,
256 kind: usize,
257 generation: u64,
258 facts: WindowFacts,
259 source: FactSource,
260 ) -> Result<u64, CommandError> {
261 let state = self.open_state(kind, generation)?;
262 if state.facts.as_ref() != Some(&facts) {
263 state.facts = Some(facts);
264 state.fact_version += 1;
265 state.fact_source = Some(source);
266 }
267 Ok(state.fact_version)
268 }
269
270 pub fn window_info(&self, kind: usize) -> Option<WindowInfo> {
272 let state = self.windows.get(kind).filter(|state| state.open)?;
273 let lifecycle = if state.pending.is_some() {
274 Lifecycle::Closing
275 } else if state.facts.is_none() {
276 Lifecycle::Opening
277 } else {
278 Lifecycle::Open
279 };
280 Some(WindowInfo {
281 window: self.instance(kind).id,
282 kind: self.spec.windows[kind].id.clone(),
283 presentation: self.spec.windows[kind].presentation,
284 lifecycle,
285 facts: state.facts.clone(),
286 version: state.fact_version,
287 changed_by: state.fact_source,
288 })
289 }
290
291 pub fn window_infos(&self) -> Vec<WindowInfo> {
293 (0..self.windows.len())
294 .filter_map(|kind| self.window_info(kind))
295 .collect()
296 }
297
298 pub fn check_action(
301 &self,
302 window: &str,
303 action: WindowAction,
304 ) -> Result<WindowInstance, CommandError> {
305 let instance = self.find_window(window).ok_or_else(|| {
306 CommandError::new("target_not_found", format!("no open window `{window}`"))
307 })?;
308 let Some(facts) = &self.windows[instance.kind].facts else {
309 return Err(CommandError::new(
310 "target_unavailable",
311 format!("`{window}` is still opening"),
312 ));
313 };
314 if !facts.actions.contains(&action) {
315 let available: Vec<_> = facts.actions.iter().map(|action| action.name()).collect();
316 return Err(CommandError::new(
317 "target_unavailable",
318 format!(
319 "`{window}` cannot {} now; available: {}",
320 action.name(),
321 if available.is_empty() {
322 "none".to_string()
323 } else {
324 available.join(", ")
325 }
326 ),
327 ));
328 }
329 Ok(instance)
330 }
331
332 pub fn window_closed(&mut self, kind: usize, generation: u64) -> bool {
335 match self.windows.get_mut(kind) {
336 Some(state) if state.open && state.generation == generation => {
337 state.open = false;
338 state.drafts.clear();
339 state.pending = None;
340 self.revision += 1;
341 true
342 }
343 _ => false,
344 }
345 }
346
347 pub fn open_windows(&self) -> Vec<WindowInstance> {
348 (0..self.windows.len())
349 .filter(|kind| self.windows[*kind].open)
350 .map(|kind| self.instance(kind))
351 .collect()
352 }
353
354 pub fn find_window(&self, id: &str) -> Option<WindowInstance> {
356 self.open_windows()
357 .into_iter()
358 .find(|window| window.id == id)
359 }
360
361 pub fn window_kind(&self, id: &str) -> Option<usize> {
362 self.spec.windows.iter().position(|kind| kind.id == id)
363 }
364
365 fn slot(&self, kind: usize, generation: u64, session: usize) -> Result<usize, CommandError> {
366 let state = self
367 .windows
368 .get(kind)
369 .ok_or_else(|| CommandError::new("target_not_found", "unknown window kind"))?;
370 if !state.open || state.generation != generation {
371 return Err(stale());
372 }
373 self.spec.windows[kind]
374 .sessions
375 .iter()
376 .position(|candidate| *candidate == session)
377 .ok_or_else(|| {
378 CommandError::new(
379 "target_not_found",
380 "this window does not own that session state",
381 )
382 })
383 }
384
385 pub fn set(
387 &mut self,
388 kind: usize,
389 generation: u64,
390 session: usize,
391 field: usize,
392 value: Value,
393 ) -> Result<u64, CommandError> {
394 let slot = self.slot(kind, generation, session)?;
395 let session_type = &self.spec.sessions[session];
396 let (name, ty, _) = session_type
397 .fields
398 .get(field)
399 .ok_or_else(|| CommandError::new("target_not_found", "unknown session field"))?;
400 value.check(ty, &self.spec.types).map_err(|error| {
401 CommandError::invalid_params(format!("{}.{name}: {error}", session_type.name))
402 })?;
403 let state = &mut self.windows[kind];
404 let current = &mut state.drafts[slot][field];
405 if *current != value {
406 *current = value;
407 state.revision += 1;
409 self.revision += 1;
410 }
411 Ok(self.revision)
412 }
413
414 fn open_state(
415 &mut self,
416 kind: usize,
417 generation: u64,
418 ) -> Result<&mut WindowState, CommandError> {
419 let state = self
420 .windows
421 .get_mut(kind)
422 .ok_or_else(|| CommandError::new("target_not_found", "unknown window kind"))?;
423 if !state.open || state.generation != generation {
424 return Err(stale());
425 }
426 Ok(state)
427 }
428
429 pub fn draft_revision(&self, kind: usize) -> Option<u64> {
431 self.windows
432 .get(kind)
433 .filter(|state| state.open)
434 .map(|state| state.revision)
435 }
436
437 pub fn pending_close(&self, kind: usize) -> Option<&Confirmation> {
440 self.windows
441 .get(kind)
442 .filter(|state| state.open)
443 .and_then(|state| state.pending.as_ref())
444 }
445
446 pub fn request_close(
450 &mut self,
451 kind: usize,
452 generation: u64,
453 needs_confirmation: bool,
454 can_save: bool,
455 ) -> Result<CloseStep, CommandError> {
456 let window = format!(
457 "{}#{generation}",
458 self.spec
459 .windows
460 .get(kind)
461 .map(|kind| kind.id.as_str())
462 .unwrap_or_default()
463 );
464 let state = self.open_state(kind, generation)?;
465 if let Some(pending) = &state.pending {
466 if pending.draft_revision == state.revision {
467 return Ok(CloseStep::Confirm(pending.clone()));
468 }
469 state.pending = None;
471 }
472 if !needs_confirmation {
473 return Ok(CloseStep::Close);
474 }
475 let revision = state.revision;
476 self.confirmations += 1;
477 let confirmation = Confirmation {
478 id: format!("{window}:c{}", self.confirmations),
479 window,
480 generation,
481 draft_revision: revision,
482 can_save,
483 };
484 self.windows[kind].pending = Some(confirmation.clone());
485 Ok(CloseStep::Confirm(confirmation))
486 }
487
488 pub fn confirm_close(
492 &mut self,
493 kind: usize,
494 generation: u64,
495 confirmation: &str,
496 decision: CloseDecision,
497 ) -> Result<DecisionStep, CommandError> {
498 let state = self.open_state(kind, generation)?;
499 let Some(pending) = state.pending.clone() else {
500 return Err(stale_confirmation(
501 "no close confirmation is pending for this window; request closing again",
502 ));
503 };
504 if pending.id != confirmation {
505 return Err(stale_confirmation(
506 "this is not the window's current close confirmation",
507 ));
508 }
509 if pending.draft_revision != state.revision {
510 state.pending = None;
511 return Err(stale_confirmation(
512 "the window's drafts changed after this confirmation; request closing again",
513 ));
514 }
515 if decision == CloseDecision::Save && !pending.can_save {
516 return Err(CommandError::invalid_params(
517 "this window declares no save command; decide Discard or Cancel",
518 ));
519 }
520 state.pending = None;
521 Ok(match decision {
522 CloseDecision::Save => DecisionStep::Save,
523 CloseDecision::Discard => DecisionStep::Close,
524 CloseDecision::Cancel => DecisionStep::Cancel,
525 })
526 }
527
528 pub fn get(
529 &self,
530 kind: usize,
531 generation: u64,
532 session: usize,
533 field: usize,
534 ) -> Result<Value, CommandError> {
535 let slot = self.slot(kind, generation, session)?;
536 self.windows[kind].drafts[slot]
537 .get(field)
538 .cloned()
539 .ok_or_else(|| CommandError::new("target_not_found", "unknown session field"))
540 }
541}
542
543pub trait SessionHandler: Send + Sync {
546 fn session_id(&self) -> String;
547 fn windows(&self) -> Vec<String>;
549 fn handle(&self, command: SessionCommand, params: Vec<Value>) -> Result<Value, CommandError>;
551}
552
553#[derive(Clone, Default)]
555pub struct Sessions {
556 inner: Arc<Mutex<BTreeMap<String, Arc<dyn SessionHandler>>>>,
557}
558
559impl Sessions {
560 pub fn new() -> Self {
561 Self::default()
562 }
563
564 pub fn register(&self, handler: Arc<dyn SessionHandler>) {
565 self.inner
566 .lock()
567 .expect("sessions lock")
568 .insert(handler.session_id(), handler);
569 }
570
571 pub fn unregister(&self, session_id: &str) {
572 self.inner.lock().expect("sessions lock").remove(session_id);
573 }
574
575 pub fn list(&self) -> Vec<Arc<dyn SessionHandler>> {
576 self.inner
577 .lock()
578 .expect("sessions lock")
579 .values()
580 .cloned()
581 .collect()
582 }
583
584 pub fn handle(
587 &self,
588 session_id: Option<&str>,
589 command: SessionCommand,
590 params: Vec<Value>,
591 ) -> Result<Value, CommandError> {
592 let handler = {
593 let sessions = self.inner.lock().expect("sessions lock");
594 match session_id {
595 Some(id) => sessions.get(id).cloned().ok_or_else(|| {
596 CommandError::new("session_not_found", format!("no live UI session `{id}`"))
597 })?,
598 None => match sessions.len() {
599 0 => {
600 return Err(CommandError::new(
601 "requires_ui_session",
602 format!("`{}` needs a live UI session", command.id()),
603 ));
604 }
605 1 => sessions.values().next().unwrap().clone(),
606 _ => {
607 return Err(CommandError::new(
608 "session_required",
609 "several UI sessions are live; name one with session_id",
610 ));
611 }
612 },
613 }
614 };
615 handler.handle(command, params)
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 fn spec() -> Arc<SessionSpec> {
624 Arc::new(SessionSpec {
625 types: TypeTable::default(),
626 sessions: vec![SessionType {
627 name: "Draft".into(),
628 fields: vec![
629 ("name".into(), Type::Text, Value::Text(String::new())),
630 ("volume".into(), Type::Float, Value::Float(50.0)),
631 ],
632 }],
633 windows: vec![
634 WindowKind {
635 id: "main".into(),
636 sessions: vec![0],
637 presentation: Presentation::Native,
638 },
639 WindowKind {
640 id: "settings".into(),
641 sessions: vec![],
642 presentation: Presentation::Native,
643 },
644 ],
645 })
646 }
647
648 #[test]
649 fn drafts_are_window_scoped_and_reset_on_reopen() {
650 let mut session = UiSession::new("s", spec());
651 let first = session.window_opened(0).unwrap();
652 assert_eq!(first.id, "main#1");
653 assert_eq!(first.drafts[0][1], Value::Float(50.0));
654 session.set(0, 1, 0, 0, Value::Text("Ada".into())).unwrap();
655 assert_eq!(
657 session.window_opened(0).unwrap().drafts[0][0],
658 Value::Text("Ada".into())
659 );
660 assert!(session.window_closed(0, 1));
661 let second = session.window_opened(0).unwrap();
662 assert_eq!(second.id, "main#2");
663 assert_eq!(second.drafts[0][0], Value::Text(String::new()));
664 }
665
666 #[test]
667 fn writes_are_schema_checked_and_stale_windows_rejected() {
668 let mut session = UiSession::new("s", spec());
669 session.window_opened(0).unwrap();
670 let error = session
671 .set(0, 1, 0, 1, Value::Text("loud".into()))
672 .unwrap_err();
673 assert_eq!(error.code, "invalid_params");
674 session.window_closed(0, 1);
675 session.window_opened(0).unwrap();
676 assert_eq!(
677 session
678 .set(0, 1, 0, 0, Value::Text("x".into()))
679 .unwrap_err()
680 .code,
681 "stale_window"
682 );
683 assert!(!session.window_closed(0, 1));
684 assert_eq!(
685 session
686 .set(1, 0, 0, 0, Value::Text("x".into()))
687 .unwrap_err()
688 .code,
689 "stale_window"
690 );
691 }
692
693 fn facts(visibility: Visibility, actions: &[WindowAction]) -> WindowFacts {
694 WindowFacts {
695 visibility,
696 size: SizeState::Normal,
697 frame: Frame {
698 x: 10.0,
699 y: 40.0,
700 width: 920.0,
701 height: 640.0,
702 },
703 focused: true,
704 actions: actions.to_vec(),
705 }
706 }
707
708 #[test]
709 fn window_facts_are_versioned_per_instance_and_reject_stale_generations() {
710 let mut session = UiSession::new("s", spec());
711 session.window_opened(0).unwrap();
712 let info = session.window_info(0).unwrap();
713 assert_eq!(info.lifecycle, Lifecycle::Opening);
714 assert_eq!(info.version, 0);
715 let visible = facts(Visibility::Visible, &[WindowAction::Minimize]);
716 assert_eq!(
717 session.window_facts(0, 1, visible.clone(), FactSource::System),
718 Ok(1)
719 );
720 assert_eq!(
722 session.window_facts(0, 1, visible, FactSource::Command),
723 Ok(1)
724 );
725 let minimized = facts(Visibility::Minimized, &[WindowAction::Restore]);
726 assert_eq!(
727 session.window_facts(0, 1, minimized.clone(), FactSource::Command),
728 Ok(2)
729 );
730 let info = session.window_info(0).unwrap();
731 assert_eq!(info.lifecycle, Lifecycle::Open);
732 assert_eq!(info.changed_by, Some(FactSource::Command));
733 assert_eq!(
734 info.facts.as_ref().unwrap().visibility,
735 Visibility::Minimized
736 );
737 session.window_closed(0, 1);
739 session.window_opened(0).unwrap();
740 assert_eq!(
741 session
742 .window_facts(0, 1, minimized, FactSource::System)
743 .unwrap_err()
744 .code,
745 "stale_window"
746 );
747 let info = session.window_info(0).unwrap();
748 assert_eq!((info.window.as_str(), info.version), ("main#2", 0));
749 assert!(info.facts.is_none());
750 }
751
752 #[test]
753 fn window_actions_follow_the_latest_facts() {
754 let mut session = UiSession::new("s", spec());
755 session.window_opened(1).unwrap();
756 assert_eq!(
757 session
758 .check_action("settings#1", WindowAction::Move)
759 .unwrap_err()
760 .message,
761 "`settings#1` is still opening"
762 );
763 session
764 .window_facts(
765 1,
766 1,
767 facts(
768 Visibility::Visible,
769 &[WindowAction::Activate, WindowAction::Move],
770 ),
771 FactSource::System,
772 )
773 .unwrap();
774 assert_eq!(
775 session
776 .check_action("settings#1", WindowAction::Move)
777 .unwrap()
778 .id,
779 "settings#1"
780 );
781 let error = session
782 .check_action("settings#1", WindowAction::Minimize)
783 .unwrap_err();
784 assert_eq!(error.code, "target_unavailable");
785 assert_eq!(
786 error.message,
787 "`settings#1` cannot minimize now; available: activate, move"
788 );
789 assert_eq!(
790 session
791 .check_action("main#1", WindowAction::Minimize)
792 .unwrap_err()
793 .code,
794 "target_not_found"
795 );
796 session.request_close(1, 1, true, false).unwrap();
798 assert_eq!(
799 session.window_info(1).unwrap().lifecycle,
800 Lifecycle::Closing
801 );
802 }
803
804 #[test]
805 fn window_info_matches_the_session_catalog() {
806 let (types, specs) = bern_protocol::session_catalog();
807 let result = &specs
808 .iter()
809 .find(|spec| spec.id == "window.list")
810 .unwrap()
811 .result;
812 let mut session = UiSession::new("s", spec());
813 session.window_opened(0).unwrap();
814 session.window_opened(1).unwrap();
815 session
816 .window_facts(
817 0,
818 1,
819 facts(Visibility::Visible, &WindowAction::ALL),
820 FactSource::System,
821 )
822 .unwrap();
823 let list = Value::List(
824 session
825 .window_infos()
826 .iter()
827 .map(WindowInfo::to_value)
828 .collect(),
829 );
830 list.check(result, &types).unwrap();
831 }
832
833 #[test]
834 fn closing_without_a_failing_check_closes_at_once() {
835 let mut session = UiSession::new("s", spec());
836 session.window_opened(1).unwrap();
837 assert_eq!(
838 session.request_close(1, 1, false, false),
839 Ok(CloseStep::Close)
840 );
841 assert_eq!(
842 session.request_close(1, 7, false, false).unwrap_err().code,
843 "stale_window"
844 );
845 }
846
847 #[test]
848 fn confirmations_merge_and_bind_to_generation_and_revision() {
849 let mut session = UiSession::new("s", spec());
850 session.window_opened(0).unwrap();
851 session
852 .set(0, 1, 0, 0, Value::Text("draft".into()))
853 .unwrap();
854 let CloseStep::Confirm(first) = session.request_close(0, 1, true, true).unwrap() else {
855 panic!("expected a confirmation")
856 };
857 assert_eq!(first.window, "main#1");
858 assert_eq!(first.draft_revision, 1);
859 assert_eq!(
860 first.decisions(),
861 vec![
862 CloseDecision::Save,
863 CloseDecision::Discard,
864 CloseDecision::Cancel
865 ]
866 );
867 assert_eq!(
869 session.request_close(0, 1, true, true),
870 Ok(CloseStep::Confirm(first.clone()))
871 );
872 assert!(session.pending_close(0).is_some());
873 assert_eq!(
875 session.confirm_close(0, 1, &first.id, CloseDecision::Cancel),
876 Ok(DecisionStep::Cancel)
877 );
878 assert!(session.pending_close(0).is_none());
879 assert_eq!(
880 session
881 .confirm_close(0, 1, &first.id, CloseDecision::Discard)
882 .unwrap_err()
883 .code,
884 "stale_confirmation"
885 );
886 let CloseStep::Confirm(second) = session.request_close(0, 1, true, true).unwrap() else {
889 panic!()
890 };
891 assert_ne!(second.id, first.id);
892 session.set(0, 1, 0, 0, Value::Text("more".into())).unwrap();
893 assert_eq!(
894 session
895 .confirm_close(0, 1, &second.id, CloseDecision::Discard)
896 .unwrap_err()
897 .code,
898 "stale_confirmation"
899 );
900 let CloseStep::Confirm(third) = session.request_close(0, 1, true, true).unwrap() else {
901 panic!()
902 };
903 assert_eq!(third.draft_revision, 2);
904 assert_eq!(
905 session.confirm_close(0, 1, &third.id, CloseDecision::Save),
906 Ok(DecisionStep::Save)
907 );
908 }
909
910 #[test]
911 fn confirmations_end_with_their_window_instance() {
912 let mut session = UiSession::new("s", spec());
913 session.window_opened(0).unwrap();
914 let CloseStep::Confirm(old) = session.request_close(0, 1, true, false).unwrap() else {
915 panic!()
916 };
917 assert_eq!(
918 session
919 .confirm_close(0, 1, &old.id, CloseDecision::Save)
920 .unwrap_err()
921 .code,
922 "invalid_params"
923 );
924 assert_eq!(
925 session.confirm_close(0, 1, &old.id, CloseDecision::Discard),
926 Ok(DecisionStep::Close)
927 );
928 assert!(session.window_closed(0, 1));
929 session.window_opened(0).unwrap();
930 assert_eq!(
932 session
933 .confirm_close(0, 1, &old.id, CloseDecision::Discard)
934 .unwrap_err()
935 .code,
936 "stale_window"
937 );
938 assert_eq!(
939 session
940 .confirm_close(0, 2, &old.id, CloseDecision::Discard)
941 .unwrap_err()
942 .code,
943 "stale_confirmation"
944 );
945 }
946
947 struct Echo(String);
948
949 impl SessionHandler for Echo {
950 fn session_id(&self) -> String {
951 self.0.clone()
952 }
953 fn windows(&self) -> Vec<String> {
954 vec![]
955 }
956 fn handle(&self, command: SessionCommand, _: Vec<Value>) -> Result<Value, CommandError> {
957 Ok(Value::Text(format!("{}:{}", self.0, command.id())))
958 }
959 }
960
961 #[test]
962 fn routing_requires_an_unambiguous_live_session() {
963 let sessions = Sessions::new();
964 assert_eq!(
965 sessions
966 .handle(None, SessionCommand::Inspect, vec![])
967 .unwrap_err()
968 .code,
969 "requires_ui_session"
970 );
971 sessions.register(Arc::new(Echo("a".into())));
972 assert_eq!(
973 sessions
974 .handle(None, SessionCommand::Inspect, vec![])
975 .unwrap(),
976 Value::Text("a:ui.inspect".into())
977 );
978 sessions.register(Arc::new(Echo("b".into())));
979 assert_eq!(
980 sessions
981 .handle(None, SessionCommand::Inspect, vec![])
982 .unwrap_err()
983 .code,
984 "session_required"
985 );
986 assert!(
987 sessions
988 .handle(Some("b"), SessionCommand::Back, vec![])
989 .is_ok()
990 );
991 assert_eq!(
992 sessions
993 .handle(Some("z"), SessionCommand::Back, vec![])
994 .unwrap_err()
995 .code,
996 "session_not_found"
997 );
998 }
999}