1use std::cell::RefCell;
11use std::marker::PhantomData;
12use std::rc::Rc;
13
14use std::cell::Cell;
15
16use tilo_actions::ActionBinding;
17use tilo_resource::{ActionKey, Binds, Edit, Effect, Invoke, Path, Resource, TryMapped};
18use ui_core::widget::{
19 Button, Checkbox, Dropdown, EditableLabel, EmptyState, NumberInput, Slider, TextInput, Toggle,
20};
21
22pub trait ActionControl<I>: Sized {
24 #[must_use]
26 fn bind_action(self, binding: ActionBinding<I>) -> Self;
27
28 #[must_use]
30 fn unavailable(self) -> Self;
31}
32
33macro_rules! action_control {
34 ($($control:ty => $input:ty, |$disabled:ident| $disable:expr;)*) => {$(
35 impl ActionControl<$input> for $control {
36 fn bind_action(self, binding: ActionBinding<$input>) -> Self {
37 self.on_action(binding)
38 }
39
40 fn unavailable(self) -> Self {
41 let $disabled = self;
42 $disable
43 }
44 }
45 )*};
46}
47
48action_control! {
49 TextInput => String, |control| control.disabled(true);
50 Checkbox => bool, |control| control.with_enabled(false);
51 Toggle => bool, |control| control.with_enabled(false);
52 Slider => f64, |control| control.with_enabled(false);
53 NumberInput => f64, |control| control.with_enabled(false);
54 Dropdown => String, |control| control.with_enabled(false);
55 ui_core::widget::ToggleGroup => usize, |control| control.with_enabled(false);
56 ui_core::widget::PictureChoice => usize, |control| control.with_enabled(false);
57 ui_core::widget::ColorSwatchPicker => usize, |control| control;
59 ui_core::KeyboardMapSelection => String, |control| control.with_enabled(false);
60 ui_core::widget::ShortcutRecorder => String, |control| control;
62 Button => (), |control| control.with_enabled(false);
63 ui_core::widget::Chip => (), |control| control;
65 ui_core::widget::ListRow => (), |control| control.with_enabled(false);
67 ui_core::widget::MediaCard => (), |control| control;
69 EmptyState => (), |control| control.with_enabled(false);
70 EditableLabel => String, |control| control;
72}
73
74thread_local! {
75 pub(super) static STRICT: Cell<bool> = const { Cell::new(false) };
78 pub(super) static UNBOUND: std::cell::RefCell<Option<Vec<String>>> =
80 const { std::cell::RefCell::new(None) };
81 pub(super) static OPENED: std::cell::RefCell<Option<Vec<tilo_resource::Surface>>> =
84 const { std::cell::RefCell::new(None) };
85}
86
87pub(crate) fn opened_section<P: Resource>() {
90 record_opened::<P>(true);
91}
92
93pub(crate) fn opened_record<P: Resource>() {
101 record_opened::<P>(false);
102}
103
104fn record_opened<P: Resource>(person: bool) {
105 OPENED.with(|opened| {
106 if let Some(opened) = opened.borrow_mut().as_mut()
107 && !opened.iter().any(|surface| surface.kind == P::KIND)
108 {
109 opened.push(tilo_resource::Surface {
110 person,
111 ..P::surface()
112 });
113 }
114 });
115}
116
117pub(crate) fn unbound<R: Resource>(what: &str, why: &str) {
122 unbound_of(R::KIND, what, why);
123}
124
125pub(crate) fn unbound_of(kind: &str, what: &str, why: &str) {
130 let collected = UNBOUND.with(|unbound| {
131 unbound.borrow_mut().as_mut().map(|unbound| {
132 unbound.push(format!("a control of `{kind}` is bound to {what}: {why}"));
133 })
134 });
135 if collected.is_some() {
136 return;
137 }
138 assert!(
139 !STRICT.with(Cell::get),
140 "a control of `{kind}` is bound to {what}, which it cannot be: {why}"
141 );
142 tracing::warn!(kind, binding = what, reason = why, "control not bound");
143}
144
145type Arguments<I> =
147 Box<dyn Fn(I) -> Result<serde_json::Map<String, serde_json::Value>, String> + Send + Sync>;
148
149#[derive(Clone)]
153pub(crate) struct ActionTable {
154 app: &'static str,
156 kind: &'static str,
157 resource: Rc<RefCell<Published>>,
158 record: Option<Rc<str>>,
161 kept_by: Option<&'static str>,
164}
165
166struct Published {
170 base: Rc<tilo_actions::ResourceSpec>,
171 overlay: Vec<tilo_actions::ActionSpec>,
172}
173
174impl Published {
175 fn find(&self, key: &ActionKey) -> Result<&tilo_actions::ActionSpec, String> {
179 if self.base.kind != key.kind {
180 return Err(format!("the resource is a `{}`", self.base.kind));
181 }
182 if let Some(app) = self.base.app().filter(|app| *app != key.app) {
183 return Err(format!("it publishes `{app}`'s actions"));
184 }
185 self.overlay
186 .iter()
187 .find(|spec| spec.id == key.action)
188 .or_else(|| self.base.actions.iter().find(|spec| spec.id == key.action))
189 .ok_or_else(|| "the resource does not publish it".to_owned())
190 }
191}
192
193impl ActionTable {
194 fn new(
195 app: &'static str,
196 kind: &'static str,
197 published: Rc<tilo_actions::ResourceSpec>,
198 ) -> Self {
199 Self {
200 app,
201 kind,
202 resource: Rc::new(RefCell::new(Published {
203 base: published,
204 overlay: Vec::new(),
205 })),
206 record: None,
207 kept_by: None,
208 }
209 }
210
211 fn route(&mut self, record: &str, editable: bool, kept_by: Option<&'static str>) {
214 self.record = editable.then(|| Rc::from(record));
215 self.kept_by = kept_by;
216 }
217
218 pub(crate) fn update(&self, published: tilo_actions::ResourceSpec) {
220 self.update_shared(Rc::new(published), Vec::new());
221 }
222
223 pub(crate) fn update_shared(
227 &self,
228 base: Rc<tilo_actions::ResourceSpec>,
229 overlay: Vec<tilo_actions::ActionSpec>,
230 ) {
231 let mut current = self.resource.borrow_mut();
232 if current.base.id == base.id && current.base.kind == base.kind {
234 *current = Published { base, overlay };
235 } else {
236 tracing::error!("action publication changed resource identity");
237 }
238 }
239
240 fn key(&self, id: &str) -> ActionKey {
241 ActionKey::new(self.app, self.kind, id)
242 }
243
244 fn spec_of(&self, key: &ActionKey, fallback: &str) -> Result<tilo_actions::ActionSpec, String> {
249 let published = self.resource.borrow();
250 match published.find(key) {
251 Err(why) if key.app == self.app && key.kind == self.kind => published
252 .find(&ActionKey::new(
253 fallback,
254 key.kind.as_str(),
255 key.action.as_str(),
256 ))
257 .cloned()
258 .map_err(|_| why),
259 found => found.cloned(),
260 }
261 }
262
263 fn binding<I: 'static>(
266 &self,
267 key: ActionKey,
268 arguments: Arguments<I>,
269 ) -> Result<ActionBinding<I>, (ActionKey, String)> {
270 self.binding_in(key, self.app, arguments)
271 }
272
273 fn binding_in<I: 'static>(
276 &self,
277 key: ActionKey,
278 opened_in: &str,
279 arguments: Arguments<I>,
280 ) -> Result<ActionBinding<I>, (ActionKey, String)> {
281 let spec = self
282 .spec_of(&key, opened_in)
283 .map_err(|why| (key.clone(), why))?;
284 let published = self.exporting(spec.clone());
285 ActionBinding::fallible(&published, &spec, arguments)
286 .map_err(|error| (key, error.to_string()))
287 }
288
289 fn invocation(
294 &self,
295 key: ActionKey,
296 opened_in: &str,
297 refusal: Option<&str>,
298 arguments: serde_json::Map<String, serde_json::Value>,
299 ) -> Result<ActionBinding<()>, (ActionKey, String)> {
300 if let Some(refusal) = refusal {
301 return Err((key, refusal.to_owned()));
302 }
303 let spec = self
304 .spec_of(&key, opened_in)
305 .map_err(|why| (key.clone(), why))?;
306 let published = self.exporting(spec.clone());
307 ActionBinding::static_json(&published, &spec, arguments)
308 .map_err(|error| (key, error.to_string()))
309 }
310
311 fn control_binding_in<I: 'static>(
319 &self,
320 key: ActionKey,
321 opened_in: &str,
322 arguments: Arguments<I>,
323 ) -> Option<ActionBinding<I>> {
324 self.binding_in(key, opened_in, arguments)
325 .map_err(|(key, why)| unbound_of(self.kind, &format!("`{key}`"), &why))
326 .ok()
327 }
328
329 fn edit_binding<I: 'static, T: Resource>(
334 &self,
335 what: &str,
336 edits: bool,
337 readonly: bool,
338 edit: Box<dyn Fn(I) -> Result<serde_json::Value, String> + Send + Sync>,
339 ) -> Option<ActionBinding<I>> {
340 if !edits {
341 unbound_of(self.kind, what, "it has no field edits");
342 return None;
343 }
344 if readonly {
345 unbound_of(
346 self.kind,
347 what,
348 "the field is read-only; only verbs change it",
349 );
350 return None;
351 }
352 let spec = match self.spec_of(&self.key(tilo_resource::FIELD_EDIT), T::APP_ID) {
356 Ok(spec) => spec,
357 Err(why) => {
358 unbound_of(self.kind, what, &why);
359 return None;
360 }
361 };
362 let published = self.exporting(spec.clone());
363 let arguments: Arguments<I> = Box::new(move |input| {
364 let edit = edit(input)?;
365 Ok(serde_json::Map::from_iter([(
366 tilo_resource::EDIT_PARAM.to_owned(),
367 edit,
368 )]))
369 });
370 ActionBinding::fallible(&published, &spec, arguments)
371 .map_err(|error| unbound_of(self.kind, what, &error.to_string()))
372 .ok()
373 }
374
375 fn set_binding<I: 'static, T: Resource>(
379 &self,
380 what: &str,
381 edits: bool,
382 readonly: bool,
383 segments: &[tilo_resource::Segment],
384 value: Box<dyn Fn(I) -> Result<serde_json::Value, String> + Send + Sync>,
385 ) -> Option<ActionBinding<I>> {
386 let path = serde_json::to_value(segments).unwrap_or_default();
387 self.edit_binding::<I, T>(
388 what,
389 edits,
390 readonly,
391 Box::new(move |input| {
392 let value = value(input)?;
393 let mut edit = serde_json::to_value(tilo_resource::EditOp::Set { value })
394 .map_err(|error| error.to_string())?;
395 if let serde_json::Value::Object(edit) = &mut edit {
396 edit.insert("path".to_owned(), path.clone());
397 }
398 Ok(edit)
399 }),
400 )
401 }
402
403 fn exporting(&self, spec: tilo_actions::ActionSpec) -> tilo_actions::ResourceSpec {
407 let published = &self.resource.borrow().base;
408 tilo_actions::ResourceSpec {
409 id: published.id.clone(),
410 semantic_identity: published.semantic_identity.clone(),
411 app_id: published.app_id.clone(),
412 kind: published.kind.clone(),
413 label: published.label.clone(),
414 presentation: None,
415 children: Vec::new(),
416 actions: vec![spec],
417 views: Vec::new(),
418 readers: published.readers,
419 resource_type: published.resource_type.clone(),
420 }
421 }
422
423 fn effect_binding<I: 'static>(
426 &self,
427 key: &ActionKey,
428 spec: Option<tilo_actions::ActionSpec>,
429 arguments: Arguments<I>,
430 ) -> Option<ActionBinding<I>> {
431 let (resource, spec) = self.effect_publication(key, spec)?;
432 ActionBinding::fallible(&resource, &spec, arguments)
433 .map_err(|error| unbound_of(&key.kind, &format!("`{key}`"), &error.to_string()))
434 .ok()
435 }
436
437 fn effect_invocation(
439 &self,
440 key: &ActionKey,
441 spec: Option<tilo_actions::ActionSpec>,
442 arguments: serde_json::Map<String, serde_json::Value>,
443 ) -> Option<ActionBinding<()>> {
444 let (resource, spec) = self.effect_publication(key, spec)?;
445 ActionBinding::static_json(&resource, &spec, arguments)
446 .map_err(|error| unbound_of(&key.kind, &format!("`{key}`"), &error.to_string()))
447 .ok()
448 }
449
450 fn effect_publication(
453 &self,
454 key: &ActionKey,
455 spec: Option<tilo_actions::ActionSpec>,
456 ) -> Option<(tilo_actions::ResourceSpec, tilo_actions::ActionSpec)> {
457 let spec = match spec {
458 Some(spec) if spec.id == key.action => spec,
459 Some(_) | None => {
460 unbound_of(
461 &key.kind,
462 &format!("`{key}`"),
463 "the view declares no such effect",
464 );
465 return None;
466 }
467 };
468 let resource = self.exporting(spec.clone());
469 Some((resource, spec))
470 }
471}
472
473pub struct Actions<R: Resource> {
477 table: ActionTable,
478 marker: PhantomData<fn() -> R>,
479}
480
481impl<R: Resource> Clone for Actions<R> {
482 fn clone(&self) -> Self {
483 Self {
484 table: self.table.clone(),
485 marker: PhantomData,
486 }
487 }
488}
489
490impl<R: Resource> Actions<R> {
491 pub(crate) fn new(app: &'static str, published: Rc<tilo_actions::ResourceSpec>) -> Self {
492 Self {
493 table: ActionTable::new(app, R::KIND, published),
494 marker: PhantomData,
495 }
496 }
497
498 pub(crate) fn routed_to(
501 mut self,
502 record: &str,
503 editable: bool,
504 kept_by: Option<&'static str>,
505 ) -> Self {
506 self.table.route(record, editable, kept_by);
507 self
508 }
509
510 #[must_use]
518 pub fn operation(
519 &self,
520 decl: &tilo_resource::ActionDecl<R>,
521 ) -> Option<tilo_actions::ActionSpec> {
522 let record = self.table.record.as_deref()?;
523 let spec = super::record::route_spec(tilo_resource::declaration_spec(decl), record);
524 Some(match self.table.kept_by {
525 Some(provider) => spec.run_as(tilo_actions::RunBy {
528 provider: provider.to_owned(),
529 resource: Some(record.to_owned()),
530 invokes: AsRef::<tilo_resource::Declaration>::as_ref(decl)
531 .invokes
532 .clone(),
533 }),
534 None => spec,
535 })
536 }
537
538 #[must_use]
541 pub fn published(&self) -> Vec<tilo_actions::ActionSpec> {
542 let published = self.table.resource.borrow();
543 let mut specs = published.overlay.clone();
544 specs.extend(
545 published
546 .base
547 .actions
548 .iter()
549 .filter(|spec| !published.overlay.iter().any(|over| over.id == spec.id))
550 .cloned(),
551 );
552 specs
553 }
554
555 #[must_use]
559 pub fn spec_binding(&self, spec: &tilo_actions::ActionSpec) -> Option<ActionBinding<()>> {
560 let resource = self.table.exporting(spec.clone());
561 let spec = resource.actions.first()?.clone();
562 ActionBinding::static_json(&resource, &spec, serde_json::Map::new())
563 .map_err(|error| tracing::warn!(%error, action = %spec.id, "control not bound"))
564 .ok()
565 }
566
567 #[must_use]
570 pub fn spec_switch_binding(
571 &self,
572 spec: &tilo_actions::ActionSpec,
573 ) -> Option<ActionBinding<bool>> {
574 let resource = self.table.exporting(spec.clone());
575 let spec = resource.actions.first()?.clone();
576 let arguments: Arguments<bool> = Box::new(|_| Ok(serde_json::Map::new()));
577 ActionBinding::fallible(&resource, &spec, arguments)
578 .map_err(|error| tracing::warn!(%error, action = %spec.id, "control not bound"))
579 .ok()
580 }
581
582 pub(crate) fn resource_id(&self) -> String {
584 self.table.resource.borrow().base.id.0.clone()
585 }
586
587 pub(crate) fn table(&self) -> ActionTable {
590 self.table.clone()
591 }
592
593 pub fn binding<I: 'static>(&self, action: impl Binds<R, I>) -> Option<ActionBinding<I>> {
597 let key = action.action_key();
598 self.table
599 .binding(key, Box::new(move |input| action.arguments(input)))
600 .ok()
601 }
602
603 pub fn invocation(&self, invoke: Invoke<R>) -> Option<ActionBinding<()>> {
606 let refusal = invoke.refusal().map(str::to_owned);
607 self.table
608 .invocation(
609 self.table.key(invoke.id),
610 R::APP_ID,
611 refusal.as_deref(),
612 invoke.arguments,
613 )
614 .ok()
615 }
616
617 pub(crate) fn control_binding<I: 'static>(
620 &self,
621 action: impl Binds<R, I>,
622 ) -> Option<ActionBinding<I>> {
623 let key = action.action_key();
624 self.table.control_binding_in(
625 key,
626 R::APP_ID,
627 Box::new(move |input| action.arguments(input)),
628 )
629 }
630
631 pub(crate) fn control_invocation(&self, invoke: Invoke<R>) -> Option<ActionBinding<()>> {
633 let refusal = invoke.refusal().map(str::to_owned);
634 let key = self.table.key(invoke.id);
635 self.table
636 .invocation(key, R::APP_ID, refusal.as_deref(), invoke.arguments)
637 .map_err(|(key, why)| unbound_of(R::KIND, &format!("`{key}`"), &why))
638 .ok()
639 }
640
641 pub(crate) fn deferred_binding(
644 &self,
645 key: ActionKey,
646 arguments: impl Fn() -> Result<serde_json::Map<String, serde_json::Value>, String>
647 + Send
648 + Sync
649 + 'static,
650 ) -> Option<ActionBinding<()>> {
651 self.table
652 .control_binding_in(key, R::APP_ID, Box::new(move |()| arguments()))
653 }
654
655 pub fn edit_binding<V: 'static>(
660 &self,
661 path: Path<R, V>,
662 edit: impl Fn(&Path<R, V>, V) -> Edit<R> + Send + Sync + 'static,
663 ) -> Option<ActionBinding<V>> {
664 self.edit_binding_with(path, move |path, value| Ok(edit(path, value)))
665 }
666
667 pub fn edit_binding_with<V: 'static, I: 'static>(
673 &self,
674 path: Path<R, V>,
675 edit: impl Fn(&Path<R, V>, I) -> Result<Edit<R>, String> + Send + Sync + 'static,
676 ) -> Option<ActionBinding<I>> {
677 let what = format!("the field {path:?}");
678 let readonly = R::shape().is_some_and(|shape| shape.readonly_at(path.segments()));
679 self.table.edit_binding::<I, R>(
680 &what,
681 R::EDITS,
682 readonly,
683 Box::new(move |input| {
684 let edit = edit(&path, input)?;
685 serde_json::to_value(&edit).map_err(|error| error.to_string())
686 }),
687 )
688 }
689
690 pub fn set_binding<V, I>(
695 &self,
696 path: Path<R, V>,
697 value: impl Fn(I) -> Result<V, String> + Send + Sync + 'static,
698 ) -> Option<ActionBinding<I>>
699 where
700 V: tilo_resource::Settable + 'static,
701 I: 'static,
702 {
703 let what = format!("the field {path:?}");
704 let readonly = R::shape().is_some_and(|shape| shape.readonly_at(path.segments()));
705 let segments = path.into_segments();
706 self.table
707 .set_binding::<I, R>(&what, R::EDITS, readonly, &segments, json_of(value))
708 }
709
710 pub fn field_binding<V>(&self, path: Path<R, V>) -> Option<ActionBinding<V>>
712 where
713 V: tilo_resource::Settable + 'static,
714 {
715 self.set_binding(path, same::<V>)
716 }
717
718 pub fn bind<I: 'static, C: ActionControl<I>>(&self, control: C, action: impl Binds<R, I>) -> C {
721 match self.control_binding(action) {
722 Some(binding) => control.bind_action(binding),
723 None => control.unavailable(),
724 }
725 }
726
727 pub fn bind_invocation<C: ActionControl<()>>(&self, control: C, invoke: Invoke<R>) -> C {
729 match self.control_invocation(invoke) {
730 Some(binding) => control.bind_action(binding),
731 None => control.unavailable(),
732 }
733 }
734
735 pub fn bind_draft<A, T, F>(
742 &self,
743 button: Button,
744 draft: &ui_core::LocalState<String>,
745 action: TryMapped<R, A, F>,
746 ) -> Button
747 where
748 A: serde::Serialize + 'static,
749 T: Into<A>,
750 F: Fn(String) -> Option<T> + Send + Sync + 'static,
751 {
752 self.bind_draft_clearing(button, draft, &[], action)
753 }
754
755 pub fn bind_draft_clearing<A, T, F>(
759 &self,
760 button: Button,
761 draft: &ui_core::LocalState<String>,
762 others: &[&ui_core::LocalState<String>],
763 action: TryMapped<R, A, F>,
764 ) -> Button
765 where
766 A: serde::Serialize + 'static,
767 T: Into<A>,
768 F: Fn(String) -> Option<T> + Send + Sync + 'static,
769 {
770 let (key, parse) = draft_arguments(action);
771 let mut drafts = vec![draft.clone()];
772 drafts.extend(others.iter().map(|other| (*other).clone()));
773 self.bind_parsed_draft(button, key, parse, drafts)
774 }
775
776 fn bind_parsed_draft(
779 &self,
780 button: Button,
781 key: ActionKey,
782 parse: DraftArguments,
783 drafts: Vec<ui_core::LocalState<String>>,
784 ) -> Button {
785 let Some(draft) = drafts.first().cloned() else {
786 return button.unavailable();
787 };
788 let ready = {
789 let parse = std::sync::Arc::clone(&parse);
790 move |text: &String| parse(text.clone()).is_ok()
791 };
792 let submitted = draft.clone();
793 let submit = move |()| {
794 let arguments = parse(submitted.get())?;
795 for draft in &drafts {
796 draft.set(String::new());
797 }
798 Ok(arguments)
799 };
800 match self
801 .table
802 .control_binding_in(key, R::APP_ID, Box::new(submit))
803 {
804 Some(binding) => button.bind_action(binding).enabled_while(&draft, ready),
805 None => button.unavailable(),
806 }
807 }
808
809 pub fn submit_draft<A, T, F>(
814 &self,
815 field: TextInput,
816 draft: &ui_core::LocalState<String>,
817 action: TryMapped<R, A, F>,
818 ) -> TextInput
819 where
820 A: serde::Serialize + 'static,
821 T: Into<A>,
822 F: Fn(String) -> Option<T> + Send + Sync + 'static,
823 {
824 let (key, parse) = draft_arguments(action);
825 self.submit_parsed_draft(field, key, parse, draft.clone())
826 }
827
828 fn submit_parsed_draft(
830 &self,
831 field: TextInput,
832 key: ActionKey,
833 parse: DraftArguments,
834 draft: ui_core::LocalState<String>,
835 ) -> TextInput {
836 let submit = move |text: String| {
837 let arguments = parse(text)?;
838 draft.set(String::new());
839 Ok(arguments)
840 };
841 match self
842 .table
843 .control_binding_in(key, R::APP_ID, Box::new(submit))
844 {
845 Some(binding) => field.on_submit_action(binding),
846 None => field,
847 }
848 }
849
850 pub fn effect_binding<E: Effect, I: 'static>(
853 &self,
854 effect: impl Binds<E, I>,
855 ) -> Option<ActionBinding<I>> {
856 let key = effect_key::<E, I>(&effect);
857 self.table.effect_binding(
858 &key,
859 E::spec(&key.action),
860 Box::new(move |input| effect.arguments(input)),
861 )
862 }
863
864 pub fn effect<E: Effect>(&self, effect: &E) -> Option<ActionBinding<()>> {
866 self.table
867 .effect_invocation(&effect.key(), E::spec(effect.id()), effect.arguments())
868 }
869
870 pub fn bind_effect<E: Effect, I: 'static, C: ActionControl<I>>(
871 &self,
872 control: C,
873 effect: impl Binds<E, I>,
874 ) -> C {
875 match self.effect_binding(effect) {
876 Some(binding) => control.bind_action(binding),
877 None => control.unavailable(),
878 }
879 }
880
881 pub fn bind_effect_invocation<E: Effect, C: ActionControl<()>>(
883 &self,
884 control: C,
885 effect: &E,
886 ) -> C {
887 match self.effect(effect) {
888 Some(binding) => control.bind_action(binding),
889 None => control.unavailable(),
890 }
891 }
892}
893
894type DraftArguments = std::sync::Arc<
897 dyn Fn(String) -> Result<serde_json::Map<String, serde_json::Value>, String> + Send + Sync,
898>;
899
900fn draft_arguments<R, A, T, F>(action: TryMapped<R, A, F>) -> (ActionKey, DraftArguments)
903where
904 R: Resource,
905 A: serde::Serialize + 'static,
906 T: Into<A>,
907 F: Fn(String) -> Option<T> + Send + Sync + 'static,
908{
909 let key = Binds::<R, String>::action_key(&action);
910 (key, std::sync::Arc::new(move |text| action.arguments(text)))
911}
912
913fn effect_key<E: Effect, I>(effect: &impl Binds<E, I>) -> ActionKey {
917 ActionKey::new(E::APP_ID, E::OPERATION, effect.action_id())
918}
919
920fn json_of<V: tilo_resource::Model, I>(
921 value: impl Fn(I) -> Result<V, String> + Send + Sync + 'static,
922) -> Box<dyn Fn(I) -> Result<serde_json::Value, String> + Send + Sync> {
923 Box::new(move |input| value(input).map(|value| value.to_json()))
924}
925
926#[allow(
927 clippy::unnecessary_wraps,
928 reason = "the shape every value conversion has"
929)]
930fn same<V>(value: V) -> Result<V, String> {
931 Ok(value)
932}