app_facade_api/
integration.rs1use 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}