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 fn resource_id(&self) -> String {
587 self.table.resource.borrow().base.id.0.clone()
588 }
589
590 pub(crate) fn table(&self) -> ActionTable {
593 self.table.clone()
594 }
595
596 pub fn binding<I: 'static>(&self, action: impl Binds<R, I>) -> Option<ActionBinding<I>> {
600 let key = action.action_key();
601 self.table
602 .binding(key, Box::new(move |input| action.arguments(input)))
603 .ok()
604 }
605
606 pub fn invocation(&self, invoke: Invoke<R>) -> Option<ActionBinding<()>> {
609 let refusal = invoke.refusal().map(str::to_owned);
610 self.table
611 .invocation(
612 self.table.key(invoke.id),
613 R::APP_ID,
614 refusal.as_deref(),
615 invoke.arguments,
616 )
617 .ok()
618 }
619
620 pub(crate) fn control_binding<I: 'static>(
623 &self,
624 action: impl Binds<R, I>,
625 ) -> Option<ActionBinding<I>> {
626 let key = action.action_key();
627 self.table.control_binding_in(
628 key,
629 R::APP_ID,
630 Box::new(move |input| action.arguments(input)),
631 )
632 }
633
634 pub(crate) fn control_invocation(&self, invoke: Invoke<R>) -> Option<ActionBinding<()>> {
636 let refusal = invoke.refusal().map(str::to_owned);
637 let key = self.table.key(invoke.id);
638 self.table
639 .invocation(key, R::APP_ID, refusal.as_deref(), invoke.arguments)
640 .map_err(|(key, why)| unbound_of(R::KIND, &format!("`{key}`"), &why))
641 .ok()
642 }
643
644 pub(crate) fn deferred_binding(
647 &self,
648 key: ActionKey,
649 arguments: impl Fn() -> Result<serde_json::Map<String, serde_json::Value>, String>
650 + Send
651 + Sync
652 + 'static,
653 ) -> Option<ActionBinding<()>> {
654 self.table
655 .control_binding_in(key, R::APP_ID, Box::new(move |()| arguments()))
656 }
657
658 pub fn edit_binding<V: 'static>(
663 &self,
664 path: Path<R, V>,
665 edit: impl Fn(&Path<R, V>, V) -> Edit<R> + Send + Sync + 'static,
666 ) -> Option<ActionBinding<V>> {
667 self.edit_binding_with(path, move |path, value| Ok(edit(path, value)))
668 }
669
670 pub fn edit_binding_with<V: 'static, I: 'static>(
676 &self,
677 path: Path<R, V>,
678 edit: impl Fn(&Path<R, V>, I) -> Result<Edit<R>, String> + Send + Sync + 'static,
679 ) -> Option<ActionBinding<I>> {
680 let what = format!("the field {path:?}");
681 let readonly = R::shape().is_some_and(|shape| shape.readonly_at(path.segments()));
682 self.table.edit_binding::<I, R>(
683 &what,
684 R::EDITS,
685 readonly,
686 Box::new(move |input| {
687 let edit = edit(&path, input)?;
688 serde_json::to_value(&edit).map_err(|error| error.to_string())
689 }),
690 )
691 }
692
693 pub fn set_binding<V, I>(
698 &self,
699 path: Path<R, V>,
700 value: impl Fn(I) -> Result<V, String> + Send + Sync + 'static,
701 ) -> Option<ActionBinding<I>>
702 where
703 V: tilo_resource::Settable + 'static,
704 I: 'static,
705 {
706 let what = format!("the field {path:?}");
707 let readonly = R::shape().is_some_and(|shape| shape.readonly_at(path.segments()));
708 let segments = path.into_segments();
709 self.table
710 .set_binding::<I, R>(&what, R::EDITS, readonly, &segments, json_of(value))
711 }
712
713 pub fn field_binding<V>(&self, path: Path<R, V>) -> Option<ActionBinding<V>>
715 where
716 V: tilo_resource::Settable + 'static,
717 {
718 self.set_binding(path, same::<V>)
719 }
720
721 pub fn bind<I: 'static, C: ActionControl<I>>(&self, control: C, action: impl Binds<R, I>) -> C {
724 match self.control_binding(action) {
725 Some(binding) => control.bind_action(binding),
726 None => control.unavailable(),
727 }
728 }
729
730 pub fn bind_invocation<C: ActionControl<()>>(&self, control: C, invoke: Invoke<R>) -> C {
732 match self.control_invocation(invoke) {
733 Some(binding) => control.bind_action(binding),
734 None => control.unavailable(),
735 }
736 }
737
738 pub fn bind_draft<A, T, F>(
745 &self,
746 button: Button,
747 draft: &ui_core::LocalState<String>,
748 action: TryMapped<R, A, F>,
749 ) -> Button
750 where
751 A: serde::Serialize + 'static,
752 T: Into<A>,
753 F: Fn(String) -> Option<T> + Send + Sync + 'static,
754 {
755 self.bind_draft_clearing(button, draft, &[], action)
756 }
757
758 pub fn bind_draft_clearing<A, T, F>(
762 &self,
763 button: Button,
764 draft: &ui_core::LocalState<String>,
765 others: &[&ui_core::LocalState<String>],
766 action: TryMapped<R, A, F>,
767 ) -> Button
768 where
769 A: serde::Serialize + 'static,
770 T: Into<A>,
771 F: Fn(String) -> Option<T> + Send + Sync + 'static,
772 {
773 let (key, parse) = draft_arguments(action);
774 let mut drafts = vec![draft.clone()];
775 drafts.extend(others.iter().map(|other| (*other).clone()));
776 self.bind_parsed_draft(button, key, parse, drafts)
777 }
778
779 fn bind_parsed_draft(
782 &self,
783 button: Button,
784 key: ActionKey,
785 parse: DraftArguments,
786 drafts: Vec<ui_core::LocalState<String>>,
787 ) -> Button {
788 let Some(draft) = drafts.first().cloned() else {
789 return button.unavailable();
790 };
791 let ready = {
792 let parse = std::sync::Arc::clone(&parse);
793 move |text: &String| parse(text.clone()).is_ok()
794 };
795 let submitted = draft.clone();
796 let submit = move |()| {
797 let arguments = parse(submitted.get())?;
798 for draft in &drafts {
799 draft.set(String::new());
800 }
801 Ok(arguments)
802 };
803 match self
804 .table
805 .control_binding_in(key, R::APP_ID, Box::new(submit))
806 {
807 Some(binding) => button.bind_action(binding).enabled_while(&draft, ready),
808 None => button.unavailable(),
809 }
810 }
811
812 pub fn submit_draft<A, T, F>(
817 &self,
818 field: TextInput,
819 draft: &ui_core::LocalState<String>,
820 action: TryMapped<R, A, F>,
821 ) -> TextInput
822 where
823 A: serde::Serialize + 'static,
824 T: Into<A>,
825 F: Fn(String) -> Option<T> + Send + Sync + 'static,
826 {
827 let (key, parse) = draft_arguments(action);
828 self.submit_parsed_draft(field, key, parse, draft.clone())
829 }
830
831 fn submit_parsed_draft(
833 &self,
834 field: TextInput,
835 key: ActionKey,
836 parse: DraftArguments,
837 draft: ui_core::LocalState<String>,
838 ) -> TextInput {
839 let submit = move |text: String| {
840 let arguments = parse(text)?;
841 draft.set(String::new());
842 Ok(arguments)
843 };
844 match self
845 .table
846 .control_binding_in(key, R::APP_ID, Box::new(submit))
847 {
848 Some(binding) => field.on_submit_action(binding),
849 None => field,
850 }
851 }
852
853 pub fn effect_binding<E: Effect, I: 'static>(
856 &self,
857 effect: impl Binds<E, I>,
858 ) -> Option<ActionBinding<I>> {
859 let key = effect_key::<E, I>(&effect);
860 self.table.effect_binding(
861 &key,
862 E::spec(&key.action),
863 Box::new(move |input| effect.arguments(input)),
864 )
865 }
866
867 pub fn effect<E: Effect>(&self, effect: &E) -> Option<ActionBinding<()>> {
869 self.table
870 .effect_invocation(&effect.key(), E::spec(effect.id()), effect.arguments())
871 }
872
873 pub fn bind_effect<E: Effect, I: 'static, C: ActionControl<I>>(
874 &self,
875 control: C,
876 effect: impl Binds<E, I>,
877 ) -> C {
878 match self.effect_binding(effect) {
879 Some(binding) => control.bind_action(binding),
880 None => control.unavailable(),
881 }
882 }
883
884 pub fn bind_effect_invocation<E: Effect, C: ActionControl<()>>(
886 &self,
887 control: C,
888 effect: &E,
889 ) -> C {
890 match self.effect(effect) {
891 Some(binding) => control.bind_action(binding),
892 None => control.unavailable(),
893 }
894 }
895}
896
897type DraftArguments = std::sync::Arc<
900 dyn Fn(String) -> Result<serde_json::Map<String, serde_json::Value>, String> + Send + Sync,
901>;
902
903fn draft_arguments<R, A, T, F>(action: TryMapped<R, A, F>) -> (ActionKey, DraftArguments)
906where
907 R: Resource,
908 A: serde::Serialize + 'static,
909 T: Into<A>,
910 F: Fn(String) -> Option<T> + Send + Sync + 'static,
911{
912 let key = Binds::<R, String>::action_key(&action);
913 (key, std::sync::Arc::new(move |text| action.arguments(text)))
914}
915
916fn effect_key<E: Effect, I>(effect: &impl Binds<E, I>) -> ActionKey {
920 ActionKey::new(E::APP_ID, E::OPERATION, effect.action_id())
921}
922
923fn json_of<V: tilo_resource::Model, I>(
924 value: impl Fn(I) -> Result<V, String> + Send + Sync + 'static,
925) -> Box<dyn Fn(I) -> Result<serde_json::Value, String> + Send + Sync> {
926 Box::new(move |input| value(input).map(|value| value.to_json()))
927}
928
929#[allow(
930 clippy::unnecessary_wraps,
931 reason = "the shape every value conversion has"
932)]
933fn same<V>(value: V) -> Result<V, String> {
934 Ok(value)
935}