1use std::collections::btree_map::Entry;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::sync::mpsc::Receiver;
5
6use semver::{Version, VersionReq};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::{ModuleId, RUNTIME_API_VERSION, SchemaValidationError, UiValue, ValueSchema};
11
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
13#[serde(try_from = "String", into = "String")]
14pub struct CapabilityId(String);
15
16impl CapabilityId {
17 pub fn parse(value: impl Into<String>) -> Result<Self, CapabilityError> {
23 let value = value.into();
24 if value
25 .split_once('.')
26 .is_some_and(|(namespace, name)| is_identifier(namespace) && is_identifier(name))
27 {
28 Ok(Self(value))
29 } else {
30 Err(CapabilityError::InvalidId(value))
31 }
32 }
33
34 #[must_use]
35 pub fn as_str(&self) -> &str {
36 &self.0
37 }
38}
39
40impl TryFrom<String> for CapabilityId {
41 type Error = CapabilityError;
42
43 fn try_from(value: String) -> Result<Self, Self::Error> {
44 Self::parse(value)
45 }
46}
47
48impl From<CapabilityId> for String {
49 fn from(value: CapabilityId) -> Self {
50 value.0
51 }
52}
53
54fn is_identifier(value: &str) -> bool {
55 !value.is_empty()
56 && !value.starts_with('_')
57 && !value.ends_with('_')
58 && !value.contains("__")
59 && value.chars().all(|character| {
60 character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
61 })
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct CapabilityMethod {
66 pub input: ValueSchema,
67 pub output: ValueSchema,
68}
69
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct CapabilityDescriptor {
72 pub id: CapabilityId,
73 pub version: Version,
74 pub methods: BTreeMap<String, CapabilityMethod>,
75}
76
77#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub enum InvocationOrigin {
84 UserInput { event: String },
87 Automation,
89 Timer { started_by: Box<InvocationOrigin> },
91 TaskCompletion { started_by: Box<InvocationOrigin> },
93 Subscription,
95 Effect,
97 #[default]
99 Lifecycle,
100}
101
102impl InvocationOrigin {
103 #[must_use]
106 pub fn root(&self) -> &Self {
107 match self {
108 Self::Timer { started_by } | Self::TaskCompletion { started_by } => started_by.root(),
109 other => other,
110 }
111 }
112
113 #[must_use]
116 pub fn is_user_input(&self) -> bool {
117 matches!(self.root(), Self::UserInput { .. })
118 }
119}
120
121#[derive(Clone, Debug, PartialEq)]
123pub struct InvocationContext {
124 pub origin: InvocationOrigin,
125 pub view_id: Option<String>,
127 pub component: crate::ComponentInstancePath,
129}
130
131impl InvocationContext {
132 fn host() -> Self {
135 Self {
136 origin: InvocationOrigin::default(),
137 view_id: None,
138 component: crate::ComponentInstancePath::root("App", "root"),
139 }
140 }
141}
142
143pub trait CapabilityHandler {
144 fn call(&mut self, method: &str, input: UiValue) -> Result<UiValue, String>;
151
152 fn call_with(
161 &mut self,
162 context: &InvocationContext,
163 method: &str,
164 input: UiValue,
165 ) -> Result<UiValue, String> {
166 let _ = context;
167 self.call(method, input)
168 }
169}
170
171pub struct TaskWork {
172 work: Box<dyn FnOnce(crate::TaskCancellation) -> Result<UiValue, String> + Send + 'static>,
173}
174
175impl TaskWork {
176 #[must_use]
177 pub fn new(work: impl FnOnce() -> Result<UiValue, String> + Send + 'static) -> Self {
178 Self {
179 work: Box::new(move |_| work()),
180 }
181 }
182
183 #[must_use]
184 pub fn cancellable(
185 work: impl FnOnce(crate::TaskCancellation) -> Result<UiValue, String> + Send + 'static,
186 ) -> Self {
187 Self {
188 work: Box::new(work),
189 }
190 }
191
192 pub(crate) fn run(self, cancellation: crate::TaskCancellation) -> Result<UiValue, String> {
193 (self.work)(cancellation)
194 }
195}
196
197pub trait AsyncCapabilityHandler {
198 fn start(&mut self, method: &str, input: UiValue) -> Result<TaskWork, String>;
204
205 fn start_with(
214 &mut self,
215 context: &InvocationContext,
216 method: &str,
217 input: UiValue,
218 ) -> Result<TaskWork, String> {
219 let _ = context;
220 self.start(method, input)
221 }
222}
223
224pub struct SubscriptionWork {
231 work: Box<dyn FnOnce(crate::SubscriptionEmitter) + Send + 'static>,
232}
233
234impl SubscriptionWork {
235 #[must_use]
237 pub fn new(work: impl FnOnce(crate::SubscriptionEmitter) + Send + 'static) -> Self {
238 Self {
239 work: Box::new(work),
240 }
241 }
242
243 #[must_use]
246 pub fn from_receiver(receiver: Receiver<UiValue>) -> Self {
247 Self::new(move |emitter| {
248 loop {
249 if emitter.close_reason().is_some() {
250 break;
251 }
252 match receiver.recv_timeout(std::time::Duration::from_millis(50)) {
253 Ok(value) => {
254 if emitter.emit_blocking(value).is_err() {
255 break;
256 }
257 }
258 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
259 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
260 }
261 }
262 })
263 }
264
265 pub(crate) fn run(self, emitter: crate::SubscriptionEmitter) {
266 (self.work)(emitter);
267 }
268}
269
270pub trait SubscriptionCapabilityHandler {
271 fn subscribe(&mut self, method: &str, input: UiValue) -> Result<SubscriptionWork, String>;
281
282 fn subscribe_with(
290 &mut self,
291 context: &InvocationContext,
292 method: &str,
293 input: UiValue,
294 ) -> Result<SubscriptionWork, String> {
295 let _ = context;
296 self.subscribe(method, input)
297 }
298}
299
300struct CapabilityEntry {
301 descriptor: CapabilityDescriptor,
302 sync: Option<Box<dyn CapabilityHandler>>,
303 task: Option<Box<dyn AsyncCapabilityHandler>>,
304 subscription: Option<Box<dyn SubscriptionCapabilityHandler>>,
305}
306
307#[derive(Default)]
308pub struct CapabilityRegistry {
309 entries: BTreeMap<CapabilityId, CapabilityEntry>,
310 active: BTreeSet<CapabilityId>,
311}
312
313impl fmt::Debug for CapabilityRegistry {
314 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315 formatter
316 .debug_struct("CapabilityRegistry")
317 .field("registered", &self.entries.keys().collect::<Vec<_>>())
318 .field("active", &self.active)
319 .finish()
320 }
321}
322
323impl CapabilityRegistry {
324 #[must_use]
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 pub fn register(
336 &mut self,
337 descriptor: CapabilityDescriptor,
338 handler: impl CapabilityHandler + 'static,
339 ) -> Result<(), CapabilityError> {
340 validate_descriptor(&descriptor)?;
341 let entry = self.entry_for_descriptor(descriptor)?;
342 if entry.sync.is_some() {
343 return Err(CapabilityError::DuplicateMode("sync"));
344 }
345 entry.sync = Some(Box::new(handler));
346 Ok(())
347 }
348
349 pub fn register_async(
355 &mut self,
356 descriptor: CapabilityDescriptor,
357 handler: impl AsyncCapabilityHandler + 'static,
358 ) -> Result<(), CapabilityError> {
359 validate_descriptor(&descriptor)?;
360 let entry = self.entry_for_descriptor(descriptor)?;
361 if entry.task.is_some() {
362 return Err(CapabilityError::DuplicateMode("task"));
363 }
364 entry.task = Some(Box::new(handler));
365 Ok(())
366 }
367
368 pub fn register_subscription(
374 &mut self,
375 descriptor: CapabilityDescriptor,
376 handler: impl SubscriptionCapabilityHandler + 'static,
377 ) -> Result<(), CapabilityError> {
378 validate_descriptor(&descriptor)?;
379 let entry = self.entry_for_descriptor(descriptor)?;
380 if entry.subscription.is_some() {
381 return Err(CapabilityError::DuplicateMode("subscription"));
382 }
383 entry.subscription = Some(Box::new(handler));
384 Ok(())
385 }
386
387 fn entry_for_descriptor(
388 &mut self,
389 descriptor: CapabilityDescriptor,
390 ) -> Result<&mut CapabilityEntry, CapabilityError> {
391 let id = descriptor.id.clone();
392 match self.entries.entry(id.clone()) {
393 Entry::Vacant(entry) => Ok(entry.insert(CapabilityEntry {
394 descriptor,
395 sync: None,
396 task: None,
397 subscription: None,
398 })),
399 Entry::Occupied(entry) => {
400 if entry.get().descriptor == descriptor {
401 Ok(entry.into_mut())
402 } else {
403 Err(CapabilityError::DescriptorMismatch(id))
404 }
405 }
406 }
407 }
408
409 pub fn activate(
416 &mut self,
417 requirements: &BTreeMap<CapabilityId, VersionReq>,
418 ) -> Result<(), CapabilityError> {
419 let mut active = BTreeSet::new();
420 for (id, requirement) in requirements {
421 let entry = self
422 .entries
423 .get(id)
424 .ok_or_else(|| CapabilityError::Missing(id.clone()))?;
425 if !requirement.matches(&entry.descriptor.version) {
426 return Err(CapabilityError::VersionMismatch {
427 id: id.clone(),
428 required: requirement.clone(),
429 actual: entry.descriptor.version.clone(),
430 });
431 }
432 active.insert(id.clone());
433 }
434 self.active = active;
435 Ok(())
436 }
437
438 pub fn call(
445 &mut self,
446 id: &CapabilityId,
447 method: &str,
448 input: UiValue,
449 ) -> Result<UiValue, CapabilityError> {
450 self.call_with(&InvocationContext::host(), id, method, input)
451 }
452
453 pub fn call_with(
459 &mut self,
460 context: &InvocationContext,
461 id: &CapabilityId,
462 method: &str,
463 input: UiValue,
464 ) -> Result<UiValue, CapabilityError> {
465 if !self.active.contains(id) {
466 return Err(CapabilityError::NotDeclared(id.clone()));
467 }
468 let entry = self
469 .entries
470 .get_mut(id)
471 .ok_or_else(|| CapabilityError::Missing(id.clone()))?;
472 let schema = entry
473 .descriptor
474 .methods
475 .get(method)
476 .cloned()
477 .ok_or_else(|| CapabilityError::UnknownMethod {
478 id: id.clone(),
479 method: method.to_owned(),
480 })?;
481 schema
482 .input
483 .validate_ui_value(&input)
484 .map_err(|source| CapabilityError::InvalidInput {
485 id: id.clone(),
486 method: method.to_owned(),
487 source,
488 })?;
489 let handler = entry
490 .sync
491 .as_mut()
492 .ok_or(CapabilityError::UnsupportedMode("sync"))?;
493 let output = handler
494 .call_with(context, method, input)
495 .map_err(|message| CapabilityError::Handler {
496 id: id.clone(),
497 method: method.to_owned(),
498 message,
499 })?;
500 schema.output.validate_ui_value(&output).map_err(|source| {
501 CapabilityError::InvalidOutput {
502 id: id.clone(),
503 method: method.to_owned(),
504 source,
505 }
506 })?;
507 Ok(output)
508 }
509
510 pub fn start_task(
516 &mut self,
517 id: &CapabilityId,
518 method: &str,
519 input: UiValue,
520 ) -> Result<(TaskWork, ValueSchema), CapabilityError> {
521 self.start_task_with(&InvocationContext::host(), id, method, input)
522 }
523
524 pub fn start_task_with(
530 &mut self,
531 context: &InvocationContext,
532 id: &CapabilityId,
533 method: &str,
534 input: UiValue,
535 ) -> Result<(TaskWork, ValueSchema), CapabilityError> {
536 let (entry, schema) = self.entry_and_method(id, method, &input)?;
537 let handler = entry
538 .task
539 .as_mut()
540 .ok_or(CapabilityError::UnsupportedMode("task"))?;
541 let work = handler
542 .start_with(context, method, input)
543 .map_err(|message| CapabilityError::Handler {
544 id: id.clone(),
545 method: method.to_owned(),
546 message,
547 })?;
548 Ok((work, schema.output))
549 }
550
551 pub fn start_subscription(
557 &mut self,
558 id: &CapabilityId,
559 method: &str,
560 input: UiValue,
561 ) -> Result<(SubscriptionWork, ValueSchema), CapabilityError> {
562 self.start_subscription_with(&InvocationContext::host(), id, method, input)
563 }
564
565 pub fn start_subscription_with(
571 &mut self,
572 context: &InvocationContext,
573 id: &CapabilityId,
574 method: &str,
575 input: UiValue,
576 ) -> Result<(SubscriptionWork, ValueSchema), CapabilityError> {
577 let (entry, schema) = self.entry_and_method(id, method, &input)?;
578 let handler = entry
579 .subscription
580 .as_mut()
581 .ok_or(CapabilityError::UnsupportedMode("subscription"))?;
582 let work = handler
583 .subscribe_with(context, method, input)
584 .map_err(|message| CapabilityError::Handler {
585 id: id.clone(),
586 method: method.to_owned(),
587 message,
588 })?;
589 Ok((work, schema.output))
590 }
591
592 fn entry_and_method(
593 &mut self,
594 id: &CapabilityId,
595 method: &str,
596 input: &UiValue,
597 ) -> Result<(&mut CapabilityEntry, CapabilityMethod), CapabilityError> {
598 if !self.active.contains(id) {
599 return Err(CapabilityError::NotDeclared(id.clone()));
600 }
601 let entry = self
602 .entries
603 .get_mut(id)
604 .ok_or_else(|| CapabilityError::Missing(id.clone()))?;
605 let schema = entry
606 .descriptor
607 .methods
608 .get(method)
609 .cloned()
610 .ok_or_else(|| CapabilityError::UnknownMethod {
611 id: id.clone(),
612 method: method.to_owned(),
613 })?;
614 schema
615 .input
616 .validate_ui_value(input)
617 .map_err(|source| CapabilityError::InvalidInput {
618 id: id.clone(),
619 method: method.to_owned(),
620 source,
621 })?;
622 Ok((entry, schema))
623 }
624}
625
626fn validate_descriptor(descriptor: &CapabilityDescriptor) -> Result<(), CapabilityError> {
627 if descriptor.methods.is_empty() {
628 return Err(CapabilityError::NoMethods(descriptor.id.clone()));
629 }
630 if let Some(method) = descriptor
631 .methods
632 .keys()
633 .find(|method| !is_identifier(method))
634 {
635 return Err(CapabilityError::InvalidMethod(method.clone()));
636 }
637 Ok(())
638}
639
640#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
641pub struct AppManifest {
642 pub entry: ModuleId,
643 pub runtime_api: u32,
644 #[serde(default)]
645 pub capabilities: BTreeMap<CapabilityId, VersionReq>,
646}
647
648impl AppManifest {
649 #[must_use]
650 pub fn new(entry: ModuleId) -> Self {
651 Self {
652 entry,
653 runtime_api: RUNTIME_API_VERSION,
654 capabilities: BTreeMap::new(),
655 }
656 }
657
658 pub fn with_capability(mut self, id: &str, requirement: &str) -> Result<Self, CapabilityError> {
664 let id = CapabilityId::parse(id)?;
665 let requirement = VersionReq::parse(requirement).map_err(|source| {
666 CapabilityError::InvalidVersionRequirement {
667 requirement: requirement.to_owned(),
668 source,
669 }
670 })?;
671 self.capabilities.insert(id, requirement);
672 Ok(self)
673 }
674
675 pub fn activate(&self, registry: &mut CapabilityRegistry) -> Result<(), CapabilityError> {
681 if self.runtime_api != RUNTIME_API_VERSION {
682 return Err(CapabilityError::RuntimeApiMismatch {
683 required: self.runtime_api,
684 actual: RUNTIME_API_VERSION,
685 });
686 }
687 registry.activate(&self.capabilities)
688 }
689
690 pub fn validate_components(
697 &self,
698 components: &crate::ComponentRegistry,
699 ) -> Result<(), CapabilityError> {
700 for (component_id, component) in components.iter() {
701 for (raw_id, required) in &component.metadata.capabilities {
702 let id = CapabilityId::parse(raw_id)?;
703 let Some(declared) = self.capabilities.get(&id) else {
704 return Err(CapabilityError::ComponentCapabilityMissing {
705 component: component_id.clone(),
706 capability: id,
707 });
708 };
709 if declared != required {
710 return Err(CapabilityError::ComponentCapabilityMismatch {
711 component: component_id.clone(),
712 capability: id,
713 component_required: required.clone(),
714 app_declared: declared.clone(),
715 });
716 }
717 }
718 }
719 Ok(())
720 }
721}
722
723#[derive(Debug, Error)]
724pub enum CapabilityError {
725 #[error("capability ID `{0}` must be a namespaced `snake_case` identifier")]
726 InvalidId(String),
727 #[error("invalid capability version requirement `{requirement}`: {source}")]
728 InvalidVersionRequirement {
729 requirement: String,
730 #[source]
731 source: semver::Error,
732 },
733 #[error("capability `{0:?}` was registered with conflicting descriptors")]
734 DescriptorMismatch(CapabilityId),
735 #[error("capability handler mode `{0}` is already registered")]
736 DuplicateMode(&'static str),
737 #[error("capability does not implement `{0}` mode")]
738 UnsupportedMode(&'static str),
739 #[error("capability `{0:?}` must declare at least one method")]
740 NoMethods(CapabilityId),
741 #[error("capability method `{0}` must be a `snake_case` identifier")]
742 InvalidMethod(String),
743 #[error("required capability `{0:?}` is not registered")]
744 Missing(CapabilityId),
745 #[error("capability `{id:?}` requires {required}, host provides {actual}")]
746 VersionMismatch {
747 id: CapabilityId,
748 required: VersionReq,
749 actual: Version,
750 },
751 #[error("capability `{0:?}` was not declared by the application manifest")]
752 NotDeclared(CapabilityId),
753 #[error("capability `{id:?}` has no method `{method}`")]
754 UnknownMethod { id: CapabilityId, method: String },
755 #[error("input for `{id:?}.{method}` is invalid: {source}")]
756 InvalidInput {
757 id: CapabilityId,
758 method: String,
759 source: SchemaValidationError,
760 },
761 #[error("output from `{id:?}.{method}` is invalid: {source}")]
762 InvalidOutput {
763 id: CapabilityId,
764 method: String,
765 source: SchemaValidationError,
766 },
767 #[error("capability `{id:?}.{method}` failed: {message}")]
768 Handler {
769 id: CapabilityId,
770 method: String,
771 message: String,
772 },
773 #[error("application requires runtime API {required}, current API is {actual}")]
774 RuntimeApiMismatch { required: u32, actual: u32 },
775 #[error(
776 "component `{component}` requires capability `{capability:?}` missing from the app manifest"
777 )]
778 ComponentCapabilityMissing {
779 component: ModuleId,
780 capability: CapabilityId,
781 },
782 #[error(
783 "component `{component}` requires capability `{capability:?}` at {component_required}, but the app declares {app_declared}"
784 )]
785 ComponentCapabilityMismatch {
786 component: ModuleId,
787 capability: CapabilityId,
788 component_required: VersionReq,
789 app_declared: VersionReq,
790 },
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::{
797 ComponentDefinition, ComponentMetadata, ComponentRegistry, ComponentSchema, RuntimeApiRange,
798 };
799
800 struct Echo;
801
802 impl CapabilityHandler for Echo {
803 fn call(&mut self, method: &str, input: UiValue) -> Result<UiValue, String> {
804 if method == "echo" {
805 Ok(input)
806 } else {
807 Err("unexpected method".to_owned())
808 }
809 }
810 }
811
812 struct AsyncEcho;
813
814 impl AsyncCapabilityHandler for AsyncEcho {
815 fn start(&mut self, method: &str, input: UiValue) -> Result<TaskWork, String> {
816 if method == "echo" {
817 Ok(TaskWork::new(move || Ok(input)))
818 } else {
819 Err("unexpected method".to_owned())
820 }
821 }
822 }
823
824 fn descriptor() -> CapabilityDescriptor {
825 CapabilityDescriptor {
826 id: CapabilityId::parse("app.echo").unwrap(),
827 version: Version::new(1, 2, 0),
828 methods: BTreeMap::from([(
829 "echo".to_owned(),
830 CapabilityMethod {
831 input: ValueSchema::string(),
832 output: ValueSchema::string(),
833 },
834 )]),
835 }
836 }
837
838 #[test]
839 fn manifest_controls_capability_access_and_versions() {
840 let mut registry = CapabilityRegistry::new();
841 registry.register(descriptor(), Echo).unwrap();
842 assert!(matches!(
843 registry.call(
844 &CapabilityId::parse("app.echo").unwrap(),
845 "echo",
846 UiValue::String("no".to_owned()),
847 ),
848 Err(CapabilityError::NotDeclared(_))
849 ));
850
851 let manifest = AppManifest {
852 entry: ModuleId::parse("main").unwrap(),
853 runtime_api: RUNTIME_API_VERSION,
854 capabilities: BTreeMap::from([(
855 CapabilityId::parse("app.echo").unwrap(),
856 VersionReq::parse("^1.0").unwrap(),
857 )]),
858 };
859 manifest.activate(&mut registry).unwrap();
860 assert_eq!(
861 registry
862 .call(
863 &CapabilityId::parse("app.echo").unwrap(),
864 "echo",
865 UiValue::String("yes".to_owned()),
866 )
867 .unwrap(),
868 UiValue::String("yes".to_owned())
869 );
870 }
871
872 #[test]
873 fn manifest_must_cover_component_capability_requirements() {
874 let component = ComponentDefinition::new(
875 ComponentMetadata {
876 id: ModuleId::parse("components/remote_status").unwrap(),
877 export: "RemoteStatus".to_owned(),
878 version: Version::new(0, 1, 0),
879 runtime_api: RuntimeApiRange::new(3, 4),
880 dependencies: BTreeSet::new(),
881 capabilities: BTreeMap::from([(
882 "app.echo".to_owned(),
883 VersionReq::parse("^1.0").unwrap(),
884 )]),
885 assets: BTreeSet::new(),
886 tokens: std::collections::BTreeSet::new(),
887 environment: std::collections::BTreeSet::new(),
888 },
889 ComponentSchema::default(),
890 )
891 .unwrap();
892 let mut components = ComponentRegistry::new();
893 components.register(component, RUNTIME_API_VERSION).unwrap();
894 let missing = AppManifest::new(ModuleId::parse("main").unwrap());
895 assert!(matches!(
896 missing.validate_components(&components),
897 Err(CapabilityError::ComponentCapabilityMissing { .. })
898 ));
899 let declared = missing.with_capability("app.echo", "^1.0").unwrap();
900 declared.validate_components(&components).unwrap();
901 }
902
903 #[test]
904 fn output_is_validated_even_for_rust_handlers() {
905 struct BadOutput;
906 impl CapabilityHandler for BadOutput {
907 fn call(&mut self, _: &str, _: UiValue) -> Result<UiValue, String> {
908 Ok(UiValue::Bool(true))
909 }
910 }
911 let mut registry = CapabilityRegistry::new();
912 registry.register(descriptor(), BadOutput).unwrap();
913 registry
914 .activate(&BTreeMap::from([(
915 CapabilityId::parse("app.echo").unwrap(),
916 VersionReq::STAR,
917 )]))
918 .unwrap();
919 assert!(matches!(
920 registry.call(
921 &CapabilityId::parse("app.echo").unwrap(),
922 "echo",
923 UiValue::String("input".to_owned()),
924 ),
925 Err(CapabilityError::InvalidOutput { .. })
926 ));
927 }
928
929 #[test]
930 fn async_handler_builds_validated_worker_without_moving_rhai_callbacks() {
931 let mut registry = CapabilityRegistry::new();
932 registry.register_async(descriptor(), AsyncEcho).unwrap();
933 registry
934 .activate(&BTreeMap::from([(
935 CapabilityId::parse("app.echo").unwrap(),
936 VersionReq::STAR,
937 )]))
938 .unwrap();
939 let (work, output) = registry
940 .start_task(
941 &CapabilityId::parse("app.echo").unwrap(),
942 "echo",
943 UiValue::String("async".to_owned()),
944 )
945 .unwrap();
946 let value = work.run(crate::TaskCancellation::default()).unwrap();
947 output.validate_ui_value(&value).unwrap();
948 assert_eq!(value, UiValue::String("async".to_owned()));
949 }
950
951 #[test]
952 fn task_and_subscription_starts_receive_the_invocation_context() {
953 type Seen = std::rc::Rc<std::cell::RefCell<Vec<InvocationOrigin>>>;
954 struct Gated(Seen);
955 impl AsyncCapabilityHandler for Gated {
956 fn start(&mut self, _: &str, _: UiValue) -> Result<TaskWork, String> {
957 Err("start_with is the entry point".to_owned())
958 }
959 fn start_with(
960 &mut self,
961 context: &InvocationContext,
962 _: &str,
963 input: UiValue,
964 ) -> Result<TaskWork, String> {
965 self.0.borrow_mut().push(context.origin.clone());
966 if !context.origin.is_user_input() {
967 return Err("needs user input".to_owned());
968 }
969 Ok(TaskWork::new(move || Ok(input)))
970 }
971 }
972 impl SubscriptionCapabilityHandler for Gated {
973 fn subscribe(&mut self, _: &str, _: UiValue) -> Result<SubscriptionWork, String> {
974 Err("subscribe_with is the entry point".to_owned())
975 }
976 fn subscribe_with(
977 &mut self,
978 context: &InvocationContext,
979 _: &str,
980 _: UiValue,
981 ) -> Result<SubscriptionWork, String> {
982 self.0.borrow_mut().push(context.origin.clone());
983 Ok(SubscriptionWork::new(|_| {}))
984 }
985 }
986
987 let seen = Seen::default();
988 let id = CapabilityId::parse("app.echo").unwrap();
989 let mut registry = CapabilityRegistry::new();
990 registry
991 .register_async(descriptor(), Gated(seen.clone()))
992 .unwrap();
993 registry
994 .register_subscription(descriptor(), Gated(seen.clone()))
995 .unwrap();
996 registry
997 .activate(&BTreeMap::from([(id.clone(), VersionReq::STAR)]))
998 .unwrap();
999 let click = InvocationContext {
1000 origin: InvocationOrigin::UserInput {
1001 event: "click".to_owned(),
1002 },
1003 view_id: Some("main".to_owned()),
1004 component: crate::ComponentInstancePath::root("App", "root"),
1005 };
1006 let input = || UiValue::String("value".to_owned());
1007 assert!(
1008 registry
1009 .start_task_with(&click, &id, "echo", input())
1010 .is_ok()
1011 );
1012 assert!(matches!(
1014 registry.start_task(&id, "echo", input()),
1015 Err(CapabilityError::Handler { message, .. }) if message == "needs user input"
1016 ));
1017 let automation = InvocationContext {
1018 origin: InvocationOrigin::Automation,
1019 ..click.clone()
1020 };
1021 assert!(
1022 registry
1023 .start_task_with(&automation, &id, "echo", input())
1024 .is_err()
1025 );
1026 registry
1027 .start_subscription_with(&automation, &id, "echo", input())
1028 .unwrap();
1029 registry.start_subscription(&id, "echo", input()).unwrap();
1030 assert_eq!(
1031 *seen.borrow(),
1032 [
1033 click.origin,
1034 InvocationOrigin::Lifecycle,
1035 InvocationOrigin::Automation,
1036 InvocationOrigin::Automation,
1037 InvocationOrigin::Lifecycle,
1038 ]
1039 );
1040 }
1041}