Skip to main content

gpui_rhai/
capability.rs

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    /// Parse a namespaced capability identifier such as `app.settings`.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`CapabilityError::InvalidId`] for invalid identifiers.
22    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/// What a script invocation runs in response to.
78///
79/// The runtime sets it on each dispatch path; script code called from that
80/// callback (including component events it emits and the render it causes)
81/// shares it. Work that completes later carries the root that started it.
82#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub enum InvocationOrigin {
84    /// A platform input event dispatched to the view (`click`, `pointer_down`,
85    /// `key_down`, ...), a Host key-binding action, or a window close.
86    UserInput { event: String },
87    /// An automation command (`AutomationCommand::Dispatch`, `Action`, ...).
88    Automation,
89    /// A timer that the root origin scheduled.
90    Timer { started_by: Box<InvocationOrigin> },
91    /// A task or asset load completing; `started_by` is the root origin.
92    TaskCompletion { started_by: Box<InvocationOrigin> },
93    /// A subscription delivering a value.
94    Subscription,
95    /// An effect's start or cleanup, or a motion timeline callback.
96    Effect,
97    /// Initialization, suspend, resume, reload or disposal.
98    #[default]
99    Lifecycle,
100}
101
102impl InvocationOrigin {
103    /// The origin with timers and task completions followed back to what
104    /// started them.
105    #[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    /// Whether the root origin is user input: the signal a Host gates
114    /// user-activated capabilities on.
115    #[must_use]
116    pub fn is_user_input(&self) -> bool {
117        matches!(self.root(), Self::UserInput { .. })
118    }
119}
120
121/// The context of one capability call.
122#[derive(Clone, Debug, PartialEq)]
123pub struct InvocationContext {
124    pub origin: InvocationOrigin,
125    /// The view whose script made the call, when it runs in one.
126    pub view_id: Option<String>,
127    /// The component instance that made the call.
128    pub component: crate::ComponentInstancePath,
129}
130
131impl InvocationContext {
132    /// The context of a call the Host makes directly through the registry:
133    /// `Lifecycle`, no view, the root component.
134    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    /// Execute a schema-validated capability method.
145    ///
146    /// # Errors
147    ///
148    /// Returns a human-readable handler error. The registry validates the
149    /// returned value against the declared output schema.
150    fn call(&mut self, method: &str, input: UiValue) -> Result<UiValue, String>;
151
152    /// Execute a method with the context of the call: what it runs in
153    /// response to, the view and the component. Override it to gate a method
154    /// on user input (`context.origin.is_user_input()`); the default forwards
155    /// to [`Self::call`].
156    ///
157    /// # Errors
158    ///
159    /// As [`Self::call`].
160    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    /// Build one-shot background work after input validation.
199    ///
200    /// # Errors
201    ///
202    /// Returns a human-readable setup error before a task is spawned.
203    fn start(&mut self, method: &str, input: UiValue) -> Result<TaskWork, String>;
204
205    /// Build the work with the context of the call that starts the task, as
206    /// [`CapabilityHandler::call_with`] receives it. Override it to gate work
207    /// on user input before it runs (`context.origin.is_user_input()`); the
208    /// default forwards to [`Self::start`].
209    ///
210    /// # Errors
211    ///
212    /// As [`Self::start`].
213    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
224/// One continuous subscription producer.
225///
226/// The work owns the producer loop and should not return until the stream is
227/// finished. Returning closes the subscription and invalidates every cloned
228/// [`crate::SubscriptionEmitter`] with
229/// [`crate::SubscriptionCloseReason::WorkReturned`].
230pub struct SubscriptionWork {
231    work: Box<dyn FnOnce(crate::SubscriptionEmitter) + Send + 'static>,
232}
233
234impl SubscriptionWork {
235    /// Build a subscription from its complete blocking producer loop.
236    #[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    /// Forward values from a channel until its senders are dropped or the
244    /// subscription is closed.
245    #[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    /// Build continuous producer work after input validation.
272    ///
273    /// The returned [`SubscriptionWork`] is the producer loop. It must remain
274    /// running for as long as external code may use the emitter; returning from
275    /// it closes the subscription immediately.
276    ///
277    /// # Errors
278    ///
279    /// Returns a human-readable setup error before subscription startup.
280    fn subscribe(&mut self, method: &str, input: UiValue) -> Result<SubscriptionWork, String>;
281
282    /// Build the producer with the context of the call that starts the
283    /// subscription: an effect start, with its view and component. The
284    /// default forwards to [`Self::subscribe`].
285    ///
286    /// # Errors
287    ///
288    /// As [`Self::subscribe`].
289    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    /// Register a host implementation and its public schema.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`CapabilityError`] for duplicate IDs, empty method sets, or
334    /// invalid method names.
335    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    /// Register one-shot task methods for a capability descriptor.
350    ///
351    /// # Errors
352    ///
353    /// Returns descriptor or duplicate-mode errors.
354    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    /// Register continuous subscription methods for a capability descriptor.
369    ///
370    /// # Errors
371    ///
372    /// Returns descriptor or duplicate-mode errors.
373    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    /// Validate and activate exactly the capabilities declared by an app.
410    ///
411    /// # Errors
412    ///
413    /// Returns [`CapabilityError::Missing`] or
414    /// [`CapabilityError::VersionMismatch`].
415    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    /// Invoke an activated method through schema validation on both sides.
439    ///
440    /// # Errors
441    ///
442    /// Returns [`CapabilityError`] for undeclared access, unknown methods,
443    /// invalid values, or a handler failure.
444    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    /// Invoke an activated method with the context of the call.
454    ///
455    /// # Errors
456    ///
457    /// As [`Self::call`].
458    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    /// Build schema-validated one-shot work for the task registry.
511    ///
512    /// # Errors
513    ///
514    /// Returns capability access, schema, mode, or handler setup errors.
515    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    /// Build one-shot work with the context of the call that starts it.
525    ///
526    /// # Errors
527    ///
528    /// As [`Self::start_task`].
529    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    /// Build schema-validated continuous work for the subscription registry.
552    ///
553    /// # Errors
554    ///
555    /// Returns capability access, schema, mode, or handler setup errors.
556    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    /// Build continuous work with the context of the call that starts it.
566    ///
567    /// # Errors
568    ///
569    /// As [`Self::start_subscription`].
570    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    /// Add one namespaced capability requirement from generated or host code.
659    ///
660    /// # Errors
661    ///
662    /// Returns identifier or semantic-version requirement parse errors.
663    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    /// Validate runtime compatibility and activate declared capabilities.
676    ///
677    /// # Errors
678    ///
679    /// Returns [`CapabilityError::RuntimeApiMismatch`] or an activation error.
680    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    /// Verify that every installed component capability is explicitly declared
691    /// by the application with the same compatibility requirement.
692    ///
693    /// # Errors
694    ///
695    /// Returns missing or mismatched component capability declarations.
696    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        // Without a context the Host call is `Lifecycle`, not user input.
1013        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}