1use std::cell::{Cell, RefCell};
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use tilo_actions::{
10 ActionAddress, ActionBinding, ActionContext, ActionError, ActionInvoker, ActionNeed,
11 ControlInvocation,
12};
13
14use super::controls::Actions;
15use tilo_resource::{Edit, Effect, Invoke, Resource};
16use ui_core::LocalState;
17use ui_core::automerge::ChangeHash;
18use ui_core::permissions::{ActionKind, LivePermissions, PermissionScope, TreePath, policy};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22enum Asked {
23 Need(ActionNeed),
24 Govern,
25}
26use ui_core::widget::Edits;
27use ui_core::widget::controls::AttributedControlInvocation;
28
29pub trait Submit<R: Resource>: Sized {
34 fn action_id(&self) -> &'static str;
36
37 fn into_invoke(self) -> Result<Invoke<R>, String>;
42
43 fn control_binding(self, actions: &Actions<R>) -> Option<ActionBinding<()>> {
46 let id = self.action_id();
47 match self.into_invoke() {
48 Ok(invoke) => actions.control_invocation(invoke),
49 Err(error) => {
50 super::controls::unbound::<R>(&format!("`{id}`"), &error);
51 None
52 }
53 }
54 }
55}
56
57impl<R: Resource> Submit<R> for Invoke<R> {
58 fn action_id(&self) -> &'static str {
59 self.id
60 }
61
62 fn into_invoke(self) -> Result<Invoke<R>, String> {
63 match self.refusal() {
64 Some(refusal) => Err(refusal.to_owned()),
65 None => Ok(self),
66 }
67 }
68}
69
70impl<R: Resource> Submit<R> for Edit<R> {
71 fn action_id(&self) -> &'static str {
72 tilo_resource::FIELD_EDIT
73 }
74
75 fn into_invoke(self) -> Result<Invoke<R>, String> {
76 Invoke::edit(&self).map_err(|error| error.to_string())
77 }
78}
79
80pub struct Dispatcher<R: Resource> {
120 core: DispatchCore,
123 group: Rc<RefCell<(usize, Vec<Invoke<R>>)>>,
125 permission_of: PermissionOf<R>,
128 submit_now: SubmitNow<R>,
131 group_now: Option<GroupNow<R>>,
134 last: Rc<RefCell<Option<Serialed<R>>>>,
137}
138
139#[derive(Clone)]
143pub(crate) struct DispatchCore {
144 app_id: &'static str,
145 resource_id: Rc<str>,
146 kind: &'static str,
148 label: &'static str,
149 resource_label: LocalState<String>,
151 me: Option<Rc<str>>,
153 custody: Option<Rc<ui_core::invocation_scope::ResourceInvocationScope>>,
158 queue: Rc<RefCell<VecDeque<AttributedControlInvocation>>>,
161 read_only: LocalState<bool>,
162 permissions: Rc<RefCell<Option<LivePermissions>>>,
167 permission_states: PermissionStates,
168 refused: LocalState<Option<Refused>>,
169 landed: LocalState<Option<Landed>>,
171 waits: Rc<RefCell<Waits>>,
174 unreadable: Rc<RefCell<Option<String>>>,
177 host: Rc<RefCell<ui_core::action_host::ActionHost>>,
178 serial: Rc<Cell<u64>>,
182 continuing: Rc<Cell<bool>>,
185 applied: Rc<Cell<bool>>,
187 opened: Rc<Cell<bool>>,
189 past: bool,
191 continuable: Rc<Cell<bool>>,
194 gesture: Rc<Cell<Option<tilo_actions::ActionId>>>,
197}
198
199type PermissionOf<R> = Rc<dyn Fn(&<R as Resource>::Action) -> ActionNeed>;
201
202type PermissionStates = Rc<RefCell<Vec<(Asked, LocalState<bool>)>>>;
204
205type Serialed<R> = (u64, <R as Resource>::Action);
207
208type GroupNow<R> = Rc<
210 dyn Fn(
211 &mut ui_core::action_host::ActionHost,
212 &mut Edits<'_>,
213 &tilo_resource::ActionGroup<<R as Resource>::Action>,
214 ActionContext,
215 ) -> Result<(), ActionError>,
216>;
217
218type SubmitNow<R> = Rc<
221 dyn Fn(
222 &mut ui_core::action_host::ActionHost,
223 &mut Edits<'_>,
224 &<R as Resource>::Action,
225 ActionContext,
226 ) -> Result<(), ActionError>,
227>;
228
229impl<R: Resource> Clone for Dispatcher<R> {
230 fn clone(&self) -> Self {
231 Self {
232 core: self.core.clone(),
233 group: Rc::clone(&self.group),
234 permission_of: Rc::clone(&self.permission_of),
235 submit_now: Rc::clone(&self.submit_now),
236 group_now: self.group_now.clone(),
237 last: Rc::clone(&self.last),
238 }
239 }
240}
241
242impl DispatchCore {
243 fn new(
244 app_id: &'static str,
245 resource_id: &str,
246 (kind, label): (&'static str, &'static str),
247 read_only: bool,
248 me: Option<&str>,
249 ) -> Self {
250 Self {
251 app_id,
252 resource_id: Rc::from(resource_id),
253 kind,
254 label,
255 resource_label: LocalState::new(label.to_owned()),
256 me: me.map(Rc::from),
257 custody: None,
258 queue: Rc::default(),
259 read_only: LocalState::new(read_only),
260 permissions: Rc::default(),
261 permission_states: Rc::default(),
262 refused: LocalState::new(None),
263 landed: LocalState::new(None),
264 waits: Rc::default(),
265 unreadable: Rc::default(),
266 host: Rc::default(),
267 serial: Rc::default(),
268 continuing: Rc::default(),
269 applied: Rc::default(),
270 opened: Rc::default(),
271 past: false,
272 continuable: Rc::default(),
273 gesture: Rc::default(),
274 }
275 }
276
277 fn for_record(
280 &self,
281 id: &str,
282 (kind, label): (&'static str, &'static str),
283 read_only: bool,
284 ) -> Self {
285 Self {
286 app_id: self.app_id,
287 me: self.me.clone(),
288 custody: self.custody.clone(),
289 resource_id: Rc::from(id),
290 kind,
291 label,
292 resource_label: LocalState::new(label.to_owned()),
293 queue: Rc::clone(&self.queue),
294 read_only: LocalState::new(read_only || self.past),
295 permissions: Rc::default(),
296 permission_states: Rc::default(),
297 refused: self.refused.clone(),
298 landed: self.landed.clone(),
299 waits: Rc::clone(&self.waits),
300 unreadable: Rc::default(),
301 host: Rc::clone(&self.host),
302 serial: Rc::default(),
303 continuing: Rc::default(),
304 applied: Rc::default(),
305 opened: Rc::default(),
306 past: self.past,
307 continuable: Rc::default(),
308 gesture: Rc::default(),
309 }
310 }
311
312 fn child(&self, id: &str, (kind, label): (&'static str, &'static str)) -> Self {
316 Self {
317 resource_id: Rc::from(id),
318 kind,
319 label,
320 resource_label: LocalState::new(label.to_owned()),
321 ..self.clone()
322 }
323 }
324
325 fn queue_invocation(
326 &self,
327 address: ActionAddress,
328 arguments: serde_json::Map<String, serde_json::Value>,
329 ) {
330 let on_behalf_of = match &self.custody {
331 Some(custody) => {
332 if let Err(error) = custody.check() {
333 self.refuse(&address.action, error);
334 return;
335 }
336 Some(custody.invocation())
337 }
338 None => super::effect::acting_for(),
339 };
340 if let Some(invocation) = on_behalf_of {
341 self.waits.borrow_mut().submitted(invocation);
342 }
343 self.queue
344 .borrow_mut()
345 .push_back((ControlInvocation { address, arguments }, on_behalf_of));
346 }
347
348 pub(crate) fn note_continuation(
349 &self,
350 invocation: tilo_actions::InvocationId,
351 action: &str,
352 result: &Result<ui_core::host_operation::ResourceOperationStart, tilo_actions::DoorError>,
353 ) {
354 let landed = match result {
355 Ok(ui_core::host_operation::ResourceOperationStart::Completed(output)) => {
356 Ok(Landed::of(action, output))
357 }
358 Ok(ui_core::host_operation::ResourceOperationStart::Accepted) => Ok(Landed {
360 action: action.to_owned(),
361 answer: None,
362 created: Vec::new(),
363 }),
364 Err(error) => Err(error.to_string()),
365 };
366 self.waits.borrow_mut().landed(invocation, landed);
367 }
368
369 fn past(&mut self) {
370 self.past = true;
371 self.read_only.set(true);
372 self.permission_states = Rc::default();
373 }
374
375 pub(crate) fn follow(&self, permissions: LivePermissions) {
376 *self.permissions.borrow_mut() = Some(permissions);
377 self.refresh_permission_states();
378 }
379
380 fn asked_state(&self, asked: Asked) -> LocalState<bool> {
381 let mut states = self.permission_states.borrow_mut();
382 if let Some((_, state)) = states.iter().find(|(held, _)| *held == asked) {
383 return state.clone();
384 }
385 let state = LocalState::new(self.may_asked(&asked));
386 states.push((asked, state.clone()));
387 state
388 }
389
390 pub(crate) fn refresh_permission_states(&self) {
391 for (asked, state) in self.permission_states.borrow().iter() {
392 let allowed = self.may_asked(asked);
393 if state.get() != allowed {
394 state.set(allowed);
395 }
396 }
397 }
398
399 fn may_asked(&self, asked: &Asked) -> bool {
400 match asked {
401 Asked::Need(need) => self.may(need),
402 Asked::Govern => self.may_govern(),
403 }
404 }
405
406 fn may(&self, need: &ActionNeed) -> bool {
407 let mut node = TreePath::root().app(self.app_id);
408 if let Some(field) = &need.field {
409 node = node.field(field);
410 }
411 self.may_scope(&PermissionScope::Action {
412 node,
413 kind: need.kind,
414 })
415 }
416
417 fn may_govern(&self) -> bool {
418 self.may_scope(&PermissionScope::governance(
419 policy::roles::governance::SETTINGS,
420 ))
421 }
422
423 pub(crate) fn may_invite(&self) -> bool {
425 self.may_scope(&PermissionScope::governance(
426 policy::roles::governance::INVITE,
427 ))
428 }
429
430 fn may_scope(&self, scope: &PermissionScope) -> bool {
431 match self.permissions.borrow().as_ref() {
432 Some(permissions) => permissions.check(scope),
433 None => !self.read_only.get(),
436 }
437 }
438
439 #[cfg(any(test, feature = "test-support"))]
440 fn refresh_read_only(&self) -> bool {
441 let writes = self.permissions.borrow().as_ref().map(|permissions| {
442 permissions.check(&PermissionScope::app(self.app_id, ActionKind::Undoable))
443 });
444 match writes {
445 Some(writes) => self.set_read_only(!writes),
446 None => false,
447 }
448 }
449
450 pub(crate) fn set_read_only(&self, read_only: bool) -> bool {
451 if self.read_only.get() == read_only {
452 return false;
453 }
454 self.read_only.set(read_only);
455 true
456 }
457
458 fn view_only(&self) -> String {
459 format!("this {} is open to view only", self.label.to_lowercase())
460 }
461
462 fn not_allowed(&self, need: &ActionNeed) -> String {
464 match (&need.field, need.kind) {
465 (Some(field), _) if field == policy::roles::COMMENTS_FIELD => {
466 format!("you may not comment on this {}", self.label.to_lowercase())
467 }
468 (None, ActionKind::Undoable) => self.view_only(),
469 _ => format!("you may not do this to this {}", self.label.to_lowercase()),
470 }
471 }
472
473 fn unreadable(&self) -> Option<String> {
475 self.unreadable.borrow().clone()
476 }
477
478 pub(crate) fn refuse(&self, action: &str, reason: String) {
479 self.refused.set(Some(Refused {
480 action: action.to_owned(),
481 reason,
482 }));
483 }
484
485 fn refusal(&self, id: &str, effect: bool) -> bool {
489 if let Some(reason) = self.unreadable()
490 && !effect
491 {
492 self.refuse(id, reason);
493 return true;
494 }
495 if let Some(custody) = &self.custody
496 && let Err(error) = custody.check()
497 {
498 self.refuse(id, error);
499 return true;
500 }
501 false
502 }
503
504 fn applies_now(&self) -> Result<(), ActionError> {
507 if self.custody.is_some() {
508 return Err(ActionError::Refused(
509 "caller-scoped effects submit document changes through queued actions".into(),
510 ));
511 }
512 if let Some(reason) = self.unreadable() {
513 return Err(ActionError::Refused(reason));
514 }
515 Ok(())
516 }
517
518 fn context(&self) -> ActionContext {
521 ActionContext::for_resource(
522 self.resource_id.as_ref(),
523 self.kind,
524 self.resource_label.get(),
525 )
526 }
527
528 fn step_context(&self, context: ActionContext, step: bool, continues: bool) -> ActionContext {
531 let mut named = self.context();
536 if let Some(invoker) = ui_core::invocation_scope::current_invoker()
537 .or_else(|| self.me.as_ref().map(|me| ActionInvoker::actor(me.as_ref())))
538 {
539 named = named.with_invoker(invoker);
540 }
541 let mut context = context.enriched_by(&named);
542 if step {
545 let gesture = self.gesture.get().filter(|_| continues).unwrap_or_default();
546 self.gesture.set(Some(gesture));
547 context = context.in_gesture(Some(gesture));
548 }
549 context
550 }
551
552 fn stepped(&self, continues: bool, continuable: bool) -> u64 {
555 let serial = self.serial.get().wrapping_add(1);
556 self.serial.set(serial);
557 self.continuable.set(continuable);
558 let first_of_event = !self.applied.replace(true);
561 self.continuing
562 .set(continues && (first_of_event || self.continuing.get()));
563 serial
564 }
565
566 fn grouped(&self) {
569 self.serial.set(self.serial.get().wrapping_add(1));
570 self.applied.set(true);
571 self.continuing.set(false);
572 }
573
574 pub(crate) fn take_committed(&self) -> Option<Vec<ChangeHash>> {
575 let bracket = self.host.borrow_mut().take_committed();
576 let continuing = self.continuing.replace(false);
577 self.opened
578 .set(bracket.is_some() && !continuing && self.continuable.get());
579 self.applied.set(false);
580 if continuing { None } else { bracket }
581 }
582}
583
584impl DispatchCore {
586 pub(crate) fn me(&self) -> Option<&str> {
588 self.me.as_deref()
589 }
590
591 pub(crate) fn note_landed(&self, action: &str, output: &tilo_actions::ActionOutput) {
592 self.landed.set(Some(Landed::of(action, output)));
593 }
594
595 pub(crate) fn set_unreadable(&self, reason: Option<String>) {
598 *self.unreadable.borrow_mut() = reason;
599 }
600
601 pub(crate) fn take_attributed(&self) -> Option<AttributedControlInvocation> {
603 self.queue.borrow_mut().pop_front()
604 }
605
606 pub(crate) fn has_pending(&self) -> bool {
607 !self.queue.borrow().is_empty()
608 }
609
610 pub(crate) fn keeps_step_open(&self) -> bool {
613 self.opened.get()
614 }
615
616 pub(crate) fn resource_id(&self) -> &str {
617 &self.resource_id
618 }
619
620 pub(crate) fn app_id(&self) -> &'static str {
621 self.app_id
622 }
623}
624
625impl<R: Resource> Dispatcher<R> {
626 pub(crate) fn core(&self) -> &DispatchCore {
628 &self.core
629 }
630
631 pub(crate) fn for_custody(
632 &self,
633 custody: ui_core::invocation_scope::ResourceInvocationScope,
634 ) -> Self {
635 let mut dispatcher = self.clone();
636 dispatcher.core.custody = Some(Rc::new(custody));
637 dispatcher.group = Rc::default();
639 dispatcher
640 }
641
642 pub(crate) fn wait_landed(
646 &self,
647 invocation: tilo_actions::InvocationId,
648 ) -> Option<std::sync::mpsc::Receiver<Result<Landed, String>>> {
649 self.core.waits.borrow_mut().wait(invocation)
650 }
651
652 pub(crate) fn new(
653 app_id: &'static str,
654 resource_id: &str,
655 read_only: bool,
656 me: Option<&str>,
657 ) -> Self {
658 Self {
659 core: DispatchCore::new(app_id, resource_id, (R::KIND, R::LABEL), read_only, me),
660 group: Rc::default(),
661 permission_of: Rc::new(tilo_resource::need_of::<R>),
662 submit_now: Rc::new(|host, edits, action, context| {
663 edits.apply(host, R::ACTION_TYPE, action, context)
664 }),
665 group_now: Some(Rc::new(|host, edits, group, context| {
666 edits.apply(
667 host,
668 &tilo_resource::group::group_type::<R>(),
669 group,
670 context,
671 )
672 })),
673 last: Rc::default(),
674 }
675 }
676
677 #[cfg(any(test, feature = "test-support"))]
684 #[must_use]
685 pub fn detached(app_id: &'static str, me: &str) -> Self {
686 Self::new(app_id, R::KIND, false, Some(me))
687 }
688
689 #[cfg(any(test, feature = "test-support"))]
697 pub fn take_asked(&self) -> Result<Vec<R::Action>, String> {
698 self.core
699 .queue
700 .borrow_mut()
701 .drain(..)
702 .map(|(invocation, _)| {
703 tilo_resource::build_action::<R>(
704 &invocation.address.action,
705 &invocation.arguments,
706 tilo_resource::origin_now_ms(),
707 )
708 })
709 .collect()
710 }
711
712 pub fn me(&self) -> Option<&str> {
717 self.core.me.as_deref()
718 }
719
720 pub(crate) fn past(mut self) -> Self {
723 self.core.past();
724 self
725 }
726
727 pub fn is_past(&self) -> bool {
732 self.core.past
733 }
734
735 pub fn read_only(&self) -> bool {
739 self.core.read_only.get()
740 }
741
742 pub fn read_only_state(&self) -> LocalState<bool> {
745 self.core.read_only.clone()
746 }
747
748 pub fn permission_state(&self, need: ActionNeed) -> LocalState<bool> {
753 self.core.asked_state(Asked::Need(need))
754 }
755
756 pub fn govern_state(&self) -> LocalState<bool> {
759 self.core.asked_state(Asked::Govern)
760 }
761
762 #[cfg(any(test, feature = "test-support"))]
765 #[must_use]
766 pub fn with_permissions(self, permissions: LivePermissions) -> Self {
767 self.core.follow(permissions);
768 self.core.refresh_read_only();
769 self
770 }
771
772 pub fn may(&self, need: &ActionNeed) -> bool {
775 self.core.may(need)
776 }
777
778 pub fn may_govern(&self) -> bool {
780 self.core.may_govern()
781 }
782
783 pub fn may_apply(&self, action: &R::Action) -> bool {
786 self.core.may(&(self.permission_of)(action))
787 }
788
789 pub fn refused(&self) -> LocalState<Option<Refused>> {
793 self.core.refused.clone()
794 }
795
796 pub fn landed(&self) -> LocalState<Option<Landed>> {
800 self.core.landed.clone()
801 }
802
803 fn not_allowed(&self, action: &R::Action) -> ActionError {
805 ActionError::Refused(self.core.not_allowed(&(self.permission_of)(action)))
806 }
807
808 pub fn effect<E: Effect>(&self, effect: &E) {
814 self.core.queue_invocation(
815 ActionAddress::new(self.core.resource_id.as_ref(), effect.id()),
816 effect.arguments(),
817 );
818 }
819
820 pub fn effect_on<E: Effect>(&self, row: &str, effect: &E) {
825 self.core
826 .queue_invocation(ActionAddress::new(row, effect.id()), serde_json::Map::new());
827 }
828
829 pub fn submit(&self, submit: impl Submit<R>) {
836 let id = submit.action_id();
837 match submit.into_invoke() {
838 Ok(invoke) => self.submit_invoke(invoke),
839 Err(error) => {
840 super::controls::unbound::<R>(&format!("`{id}`"), &error);
841 self.core.refuse(id, error);
842 }
843 }
844 }
845
846 fn submit_invoke(&self, invoke: Invoke<R>) {
847 if self.core.refusal(invoke.id, invoke.is_effect()) {
848 return;
849 }
850 if invoke.is_effect() {
851 self.queue(invoke);
852 return;
853 }
854 if !self.may_invoke(&invoke) {
855 self.core.refuse(invoke.id, self.core.view_only());
856 return;
857 }
858 {
859 let mut group = self.group.borrow_mut();
860 if group.0 > 0 {
861 group.1.push(invoke);
862 return;
863 }
864 }
865 self.queue(invoke);
866 }
867
868 fn queue(&self, invoke: Invoke<R>) {
869 self.core.queue_invocation(
870 ActionAddress::new(self.core.resource_id.as_ref(), invoke.id),
871 invoke.arguments,
872 );
873 }
874
875 fn may_invoke(&self, invoke: &Invoke<R>) -> bool {
880 if invoke.id == tilo_resource::APPLY || invoke.id == tilo_resource::FIELD_EDIT {
881 return match invoke.build(0) {
882 Ok(action) => self.may_apply(&action),
883 Err(_) => !self.read_only(),
884 };
885 }
886 may_declared(
887 &self.core,
888 &tilo_resource::vocabulary::<R>().declarations(),
889 invoke.id,
890 invoke.is_group(),
891 )
892 }
893
894 pub fn open_search(&self, request: &ui_core::search::SearchRequest) {
898 self.core.queue.borrow_mut().push_back((
899 ui_core::search::open_search_invocation(request),
900 super::effect::acting_for(),
901 ));
902 }
903
904 pub fn share_link(&self, resource: &str) {
910 self.core.queue_invocation(
911 ActionAddress::new(resource, tilo_resource::share::SHARE_LINK),
912 serde_json::Map::new(),
913 );
914 }
915
916 pub(crate) fn invoke_at(
919 &self,
920 address: ActionAddress,
921 arguments: serde_json::Map<String, serde_json::Value>,
922 ) {
923 if self.read_only() {
924 return;
925 }
926 self.core.queue_invocation(address, arguments);
927 }
928
929 pub fn apply(&self, action: &R::Action) {
933 match Invoke::<R>::apply(action) {
934 Ok(invoke) => self.submit_invoke(invoke),
935 Err(error) => self.core.refuse(tilo_resource::APPLY, error.to_string()),
936 }
937 }
938
939 pub fn group<T>(&self, edits: impl FnOnce() -> T) -> T {
948 let _group = self.begin_group();
949 edits()
950 }
951
952 #[must_use = "the group submits when this guard drops"]
956 pub fn begin_group(&self) -> DispatchGroup<R> {
957 let mut group = self.group.borrow_mut();
958 group.0 = group.0.saturating_add(1);
959 drop(group);
960 DispatchGroup {
961 dispatcher: self.clone(),
962 }
963 }
964
965 fn end_group(&self) {
966 let members = {
967 let mut group = self.group.borrow_mut();
968 group.0 = group.0.saturating_sub(1);
969 if group.0 > 0 {
970 return;
971 }
972 std::mem::take(&mut group.1)
973 };
974 let mut members = members.into_iter();
975 match (members.next(), members.next()) {
976 (None, _) => {}
977 (Some(only), None) => self.queue(only),
978 (Some(first), Some(second)) => {
979 self.queue(Invoke::group([first, second].into_iter().chain(members)));
980 }
981 }
982 }
983
984 pub fn submit_now(
995 &self,
996 edits: &mut Edits<'_>,
997 submit: impl Submit<R>,
998 ) -> Result<(), ActionError> {
999 let invoke = submit.into_invoke().map_err(ActionError::Refused)?;
1000 if invoke.is_group() {
1001 return self.group_now(edits, &invoke);
1002 }
1003 let action = invoke
1004 .build(tilo_resource::origin_now_ms())
1005 .map_err(ActionError::Refused)?;
1006 if asks(&action) {
1007 return self.ask_first(invoke, &action);
1008 }
1009 self.apply_now(edits, &action)
1010 }
1011
1012 fn ask_first(&self, invoke: Invoke<R>, action: &R::Action) -> Result<(), ActionError> {
1015 if let Some(reason) = self.core.unreadable() {
1016 return Err(ActionError::Refused(reason));
1017 }
1018 if !self.may_apply(action) {
1019 return Err(self.not_allowed(action));
1020 }
1021 self.submit_invoke(invoke);
1022 Ok(())
1023 }
1024
1025 fn group_now(&self, edits: &mut Edits<'_>, invoke: &Invoke<R>) -> Result<(), ActionError> {
1029 let Some(group_now) = &self.group_now else {
1030 return Err(ActionError::Refused(
1031 "this record's edits are applied one at a time".into(),
1032 ));
1033 };
1034 self.core.applies_now()?;
1035 let group = tilo_resource::group::build_group::<R>(
1036 &invoke.arguments,
1037 tilo_resource::origin_now_ms(),
1038 )
1039 .map_err(ActionError::Refused)?;
1040 if let Some(refused) = group.actions.iter().find(|action| !self.may_apply(action)) {
1041 return Err(self.not_allowed(refused));
1042 }
1043 if group.actions.iter().any(asks) {
1045 self.submit_invoke(invoke.clone());
1046 return Ok(());
1047 }
1048 let mut context = self.core.context();
1049 if let Some(me) = &self.core.me {
1050 context = context.with_invoker(ActionInvoker::actor(me.as_ref()));
1051 }
1052 group_now(&mut self.core.host.borrow_mut(), edits, &group, context)?;
1053 *self.last.borrow_mut() = None;
1055 self.core.grouped();
1056 Ok(())
1057 }
1058
1059 pub fn apply_now(&self, edits: &mut Edits<'_>, action: &R::Action) -> Result<(), ActionError> {
1072 self.apply_now_as(edits, action, self.core.context(), false)
1073 }
1074
1075 pub fn apply_confirmed(
1082 &self,
1083 edits: &mut Edits<'_>,
1084 action: &R::Action,
1085 ) -> Result<(), ActionError> {
1086 self.apply_now_as(edits, action, self.core.context(), true)
1087 }
1088
1089 fn apply_now_as(
1090 &self,
1091 edits: &mut Edits<'_>,
1092 action: &R::Action,
1093 context: ActionContext,
1094 confirmed: bool,
1095 ) -> Result<(), ActionError> {
1096 self.core.applies_now()?;
1097 if !self.may_apply(action) {
1098 return Err(self.not_allowed(action));
1099 }
1100 if asks(action) && !confirmed {
1103 self.apply(action);
1104 return Ok(());
1105 }
1106 let step = tilo_actions::Action::change_kind(action).is_step();
1109 let serial = self.core.serial.get();
1112 let continues = step
1113 && self
1114 .last
1115 .borrow()
1116 .as_ref()
1117 .is_some_and(|(at, previous)| *at == serial && R::coalesces(previous, action));
1118 let context = self.core.step_context(context, step, continues);
1119 (self.submit_now)(&mut self.core.host.borrow_mut(), edits, action, context)?;
1120 if !step {
1121 return Ok(());
1122 }
1123 let serial = self.core.stepped(continues, R::continuable(action));
1124 *self.last.borrow_mut() = Some((serial, action.clone()));
1125 Ok(())
1126 }
1127
1128 pub(crate) fn for_record<C: Resource>(
1134 &self,
1135 id: &str,
1136 read_only: bool,
1137 apply: impl Fn(&C::Action) -> Result<(), ActionError> + 'static,
1138 ) -> Dispatcher<C> {
1139 Dispatcher {
1140 core: self.core.for_record(id, (C::KIND, C::LABEL), read_only),
1141 group: Rc::default(),
1142 permission_of: Rc::new(|_: &C::Action| ActionNeed::of(tilo_actions::ActionKind::Read)),
1143 submit_now: Rc::new(move |_, _, action: &C::Action, _| apply(action)),
1144 group_now: None,
1145 last: Rc::default(),
1146 }
1147 }
1148
1149 #[must_use]
1164 pub fn child<C: Resource>(
1165 &self,
1166 id: &str,
1167 wrap: impl Fn(C::Action) -> R::Action + 'static,
1168 ) -> Dispatcher<C> {
1169 let parent = Rc::clone(&self.submit_now);
1170 let parent_permission = Rc::clone(&self.permission_of);
1171 let wrap = Rc::new(wrap);
1172 let wrap_for_permission = Rc::clone(&wrap);
1173 Dispatcher {
1174 core: self.core.child(id, (C::KIND, C::LABEL)),
1175 group: Rc::default(),
1176 permission_of: Rc::new(move |action: &C::Action| {
1177 parent_permission(&wrap_for_permission(action.clone()))
1178 }),
1179 group_now: None,
1182 submit_now: Rc::new(move |host, edits, action: &C::Action, context| {
1183 parent(host, edits, &wrap(action.clone()), context)
1184 }),
1185 last: Rc::default(),
1186 }
1187 }
1188
1189 pub fn settle(&self) {
1195 self.last.borrow_mut().take();
1196 }
1197
1198 pub(crate) fn name_resource(&self, label: &str) {
1199 self.core.resource_label.set(label.to_owned());
1200 }
1201
1202 pub fn app_id(&self) -> &'static str {
1205 self.core.app_id
1206 }
1207}
1208
1209fn may_declared(
1213 core: &DispatchCore,
1214 declarations: &[tilo_resource::Declaration],
1215 id: &str,
1216 group: bool,
1217) -> bool {
1218 match declarations.iter().find(|decl| decl.id == id) {
1219 Some(decl) => core.may(&ActionNeed {
1220 kind: decl.kind,
1221 field: decl.field.map(str::to_owned),
1222 privileged: false,
1223 }),
1224 None if group => !core.read_only.get(),
1225 None => core.may(&ActionNeed::of(ActionKind::Read)),
1226 }
1227}
1228
1229#[derive(Clone, Debug, PartialEq, Eq)]
1231pub struct Refused {
1232 pub action: String,
1234 pub reason: String,
1236}
1237
1238#[derive(Clone, Debug, PartialEq)]
1240pub struct Landed {
1241 pub action: String,
1243 pub answer: Option<serde_json::Value>,
1245 pub created: Vec<String>,
1247}
1248
1249impl Landed {
1250 fn of(action: &str, output: &tilo_actions::ActionOutput) -> Self {
1251 Self {
1252 action: action.to_owned(),
1253 answer: output.value.clone(),
1254 created: output.resources.clone(),
1255 }
1256 }
1257
1258 #[must_use]
1261 pub fn minted(&self) -> Vec<tilo_resource::Id> {
1262 self.created
1263 .iter()
1264 .filter_map(|resource| resource.rsplit('/').next())
1265 .map(tilo_resource::Id::from_raw)
1266 .collect()
1267 }
1268}
1269
1270fn asks<A: tilo_actions::Action>(action: &A) -> bool {
1272 tilo_actions::Action::risk(action).requires_confirmation()
1273}
1274
1275#[derive(Default)]
1278struct Waits(std::collections::HashMap<tilo_actions::InvocationId, Wait>);
1279
1280#[derive(Default)]
1281struct Wait {
1282 outstanding: usize,
1284 result: Option<Result<Landed, String>>,
1286 waiter: Option<std::sync::mpsc::Sender<Result<Landed, String>>>,
1287}
1288
1289impl Waits {
1290 fn submitted(&mut self, invocation: tilo_actions::InvocationId) {
1291 let wait = self.0.entry(invocation).or_default();
1292 wait.outstanding = wait.outstanding.saturating_add(1);
1293 }
1294
1295 fn landed(&mut self, invocation: tilo_actions::InvocationId, landed: Result<Landed, String>) {
1296 let Some(wait) = self.0.get_mut(&invocation) else {
1297 return;
1298 };
1299 wait.outstanding = wait.outstanding.saturating_sub(1);
1300 if !matches!(wait.result, Some(Err(_))) {
1302 wait.result = Some(landed);
1303 }
1304 self.settle(invocation);
1305 }
1306
1307 fn wait(
1308 &mut self,
1309 invocation: tilo_actions::InvocationId,
1310 ) -> Option<std::sync::mpsc::Receiver<Result<Landed, String>>> {
1311 let wait = self.0.get_mut(&invocation)?;
1312 let (sender, receiver) = std::sync::mpsc::channel();
1313 wait.waiter = Some(sender);
1314 self.settle(invocation);
1315 Some(receiver)
1316 }
1317
1318 fn settle(&mut self, invocation: tilo_actions::InvocationId) {
1321 let Some(wait) = self.0.get_mut(&invocation) else {
1322 return;
1323 };
1324 if wait.outstanding > 0 {
1325 return;
1326 }
1327 if let Some(waiter) = wait.waiter.take()
1328 && let Some(result) = wait.result.take()
1329 {
1330 let _ = waiter.send(result);
1331 }
1332 self.0.remove(&invocation);
1333 }
1334}
1335
1336pub struct DispatchGroup<R: Resource> {
1339 dispatcher: Dispatcher<R>,
1340}
1341
1342impl<R: Resource> Drop for DispatchGroup<R> {
1343 fn drop(&mut self) {
1344 self.dispatcher.end_group();
1345 }
1346}