1use bitflags::bitflags;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct NodeAppInfo {
13 pub name: String,
15 pub version: String,
17 pub author: String,
19 pub description: String,
21 pub capabilities: Vec<String>,
23}
24
25impl NodeAppInfo {
26 pub fn capability_flags(&self) -> Capabilities {
28 let mut flags = Capabilities::empty();
29 for cap in &self.capabilities {
30 match cap.as_str() {
31 "http_handler" => flags |= Capabilities::HTTP_HANDLER,
32 "event_listener" => flags |= Capabilities::EVENT_LISTENER,
33 "filesystem_read" => flags |= Capabilities::FILESYSTEM_READ,
34 "filesystem_write" => flags |= Capabilities::FILESYSTEM_WRITE,
35 "network_outbound" => flags |= Capabilities::NETWORK_OUTBOUND,
36 "system_info" => flags |= Capabilities::SYSTEM_INFO,
37 "capability_provider" => flags |= Capabilities::CAPABILITY_PROVIDER,
38 _ => {
39 eprintln!("[node-app-api] Unknown capability '{}', ignoring", cap);
40 }
41 }
42 }
43 flags
44 }
45}
46
47bitflags! {
48 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50 pub struct Capabilities: u32 {
51 const HTTP_HANDLER = 0b0000_0001; const EVENT_LISTENER = 0b0000_0010; const FILESYSTEM_READ = 0b0000_0100; const FILESYSTEM_WRITE = 0b0000_1000; const NETWORK_OUTBOUND = 0b0001_0000; const SYSTEM_INFO = 0b0010_0000; const CAPABILITY_PROVIDER = 0b0100_0000; }
66}
67
68impl Capabilities {
69 pub fn to_string_list(&self) -> Vec<String> {
71 let mut list = Vec::new();
72 if self.contains(Capabilities::HTTP_HANDLER) {
73 list.push("http_handler".to_string());
74 }
75 if self.contains(Capabilities::EVENT_LISTENER) {
76 list.push("event_listener".to_string());
77 }
78 if self.contains(Capabilities::FILESYSTEM_READ) {
79 list.push("filesystem_read".to_string());
80 }
81 if self.contains(Capabilities::FILESYSTEM_WRITE) {
82 list.push("filesystem_write".to_string());
83 }
84 if self.contains(Capabilities::NETWORK_OUTBOUND) {
85 list.push("network_outbound".to_string());
86 }
87 if self.contains(Capabilities::SYSTEM_INFO) {
88 list.push("system_info".to_string());
89 }
90 if self.contains(Capabilities::CAPABILITY_PROVIDER) {
91 list.push("capability_provider".to_string());
92 }
93 list
94 }
95
96 pub fn from_bits_safe(bits: u32) -> Self {
98 Capabilities::from_bits_truncate(bits)
99 }
100
101 pub fn from_string(s: &str) -> Option<Self> {
103 match s.to_lowercase().as_str() {
104 "http_handler" => Some(Capabilities::HTTP_HANDLER),
105 "event_listener" => Some(Capabilities::EVENT_LISTENER),
106 "filesystem_read" => Some(Capabilities::FILESYSTEM_READ),
107 "filesystem_write" => Some(Capabilities::FILESYSTEM_WRITE),
108 "network_outbound" => Some(Capabilities::NETWORK_OUTBOUND),
109 "system_info" => Some(Capabilities::SYSTEM_INFO),
110 "capability_provider" => Some(Capabilities::CAPABILITY_PROVIDER),
111 _ => None,
112 }
113 }
114
115 pub fn all_capability_names() -> &'static [&'static str] {
117 &[
118 "http_handler",
119 "event_listener",
120 "filesystem_read",
121 "filesystem_write",
122 "network_outbound",
123 "system_info",
124 "capability_provider",
125 ]
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct CallerContext {
134 pub app_name: String,
136 pub granted_permissions: Vec<String>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct AppRequest {
143 pub id: String,
145 pub method: String,
147 pub path: String,
149 pub headers: HashMap<String, String>,
151 pub body: serde_json::Value,
153 #[serde(default)]
156 pub caller: Option<CallerContext>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct AppResponse {
162 pub status: u16,
164 pub headers: HashMap<String, String>,
166 pub body: serde_json::Value,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AppEvent {
173 pub name: String,
175 pub data: serde_json::Value,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
184pub struct CapabilityRequest {
185 pub id: String,
187 pub capability: String,
189 pub payload: serde_json::Value,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub timeout_ms: Option<u64>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub trace_id: Option<String>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub span_id: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub parent_span_id: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub trace_depth: Option<u8>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub caller_app: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub invocation_context_id: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub lease_secs: Option<u64>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct CapabilityResponse {
227 pub id: String,
229 pub success: bool,
231 pub payload: serde_json::Value,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ProvidedCapability {
239 pub name: String,
241 pub description: String,
243 #[serde(default)]
245 pub request_schema: Option<serde_json::Value>,
246 #[serde(default)]
248 pub response_schema: Option<serde_json::Value>,
249 #[serde(default = "default_capability_priority")]
251 pub priority: i32,
252 #[serde(default)]
254 pub examples: Vec<CapabilityExample>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct CapabilityExample {
260 pub label: String,
262 pub request: serde_json::Value,
264}
265
266fn default_capability_priority() -> i32 {
267 100
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn capability_request_roundtrips_without_lease_secs() {
276 let json_without_field = serde_json::json!({
279 "id": "req-1",
280 "capability": "core.test",
281 "payload": {}
282 });
283 let request: CapabilityRequest = serde_json::from_value(json_without_field)
284 .expect("deserializes without lease_secs");
285 assert_eq!(request.lease_secs, None);
286
287 let serialized = serde_json::to_string(&request).expect("serializes");
290 assert!(
291 !serialized.contains("lease_secs"),
292 "lease_secs key must be absent when None: {serialized}"
293 );
294
295 let leased = CapabilityRequest {
297 lease_secs: Some(30),
298 ..request
299 };
300 let serialized_leased = serde_json::to_string(&leased).expect("serializes");
301 assert!(
302 serialized_leased.contains("\"lease_secs\":30"),
303 "lease_secs key must be present when Some: {serialized_leased}"
304 );
305 }
306}