Skip to main content

app_facade_api/
integration.rs

1//! Capability presentation metadata shared by all Integration views.
2//!
3//! IDs are fully qualified. `parentCapabilityId` is the direct schema override,
4//! never a self-reference. The server resolves presentation fields, including
5//! `tag`, before returning a descriptor. Missing tag means no primary tag.
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct CapabilityDescriptor {
12    pub capability_id: String,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub parent_capability_id: Option<String>,
15    #[serde(rename = "type")]
16    pub kind: String,
17    pub display_name: String,
18    pub description: String,
19    pub visibility: String,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub tag: Option<String>,
22}
23
24impl CapabilityDescriptor {
25    pub fn validate(&self) -> Result<(), &'static str> {
26        if !valid_capability_id(&self.capability_id) {
27            return Err("capabilityId must be a fully qualified capability ID");
28        }
29        if let Some(parent) = &self.parent_capability_id {
30            if !valid_capability_id(parent) || parent == &self.capability_id {
31                return Err("parentCapabilityId must identify another capability");
32            }
33            if parent
34                .split('.')
35                .skip(2)
36                .ne(self.capability_id.split('.').skip(2))
37            {
38                return Err("parentCapabilityId must have the same method and capability name");
39            }
40        }
41        let method = self.capability_id.split('.').nth(2).unwrap_or_default();
42        if self.kind != method.to_ascii_uppercase() {
43            return Err("type must match the capability method");
44        }
45        if !matches!(self.visibility.as_str(), "full" | "internal") {
46            return Err("invalid capability visibility");
47        }
48        if self
49            .tag
50            .as_ref()
51            .is_some_and(|tag| tag.is_empty() || tag.trim() != tag)
52        {
53            return Err("tag must be a non-empty tag reference");
54        }
55        Ok(())
56    }
57}
58
59fn valid_capability_id(id: &str) -> bool {
60    let parts: Vec<_> = id.split('.').collect();
61    parts.len() == 4
62        && parts
63            .iter()
64            .all(|part| !part.is_empty() && !part.chars().any(char::is_whitespace))
65        && matches!(parts[2], "command" | "state" | "event" | "data")
66}