1use std::collections::BTreeMap;
9
10use serde_json::Value;
11
12use crate::{LocalizedString, constants::CAP_FILESYSTEM};
13
14pub type Constraint = Value;
19
20#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
23pub struct CapabilityRequest {
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub description: Option<LocalizedString>,
27 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
29 pub params: BTreeMap<String, Value>,
30 #[serde(default, alias = "allow", skip_serializing_if = "Vec::is_empty")]
33 pub constraints: Vec<Constraint>,
34}
35
36impl CapabilityRequest {
37 pub fn constraints_as<T: serde::de::DeserializeOwned>(
40 &self,
41 ) -> Result<Vec<T>, serde_json::Error> {
42 self.constraints
43 .iter()
44 .map(|c| serde_json::from_value::<T>(c.clone()))
45 .collect()
46 }
47}
48
49#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
52#[serde(transparent)]
53pub struct Capabilities(pub BTreeMap<String, CapabilityRequest>);
54
55impl Capabilities {
56 pub fn is_empty(&self) -> bool {
58 self.0.is_empty()
59 }
60
61 pub fn has(&self, id: &str) -> bool {
63 self.0.contains_key(id)
64 }
65
66 pub fn get(&self, id: &str) -> Option<&CapabilityRequest> {
68 self.0.get(id)
69 }
70
71 pub fn fs_mount_root(&self) -> Option<&str> {
73 self.0
74 .get(CAP_FILESYSTEM)?
75 .params
76 .get("mount-root")?
77 .as_str()
78 }
79
80 pub fn iter(&self) -> impl Iterator<Item = (&String, &CapabilityRequest)> {
82 self.0.iter()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn request_serde_skips_empty_and_aliases_allow() {
92 let req = CapabilityRequest {
93 constraints: vec![serde_json::json!({ "host": "*" })],
94 ..Default::default()
95 };
96 let v = serde_json::to_value(&req).unwrap();
97 assert_eq!(v, serde_json::json!({ "constraints": [{ "host": "*" }] }));
98
99 let from_allow: CapabilityRequest =
101 serde_json::from_value(serde_json::json!({ "allow": [{ "host": "x" }] })).unwrap();
102 assert_eq!(
103 from_allow.constraints,
104 vec![serde_json::json!({ "host": "x" })]
105 );
106 }
107
108 #[test]
109 fn constraints_as_parses_typed() {
110 use crate::FilesystemAllow;
111 let req = CapabilityRequest {
112 constraints: vec![serde_json::json!({ "path": "/x/**", "mode": "rw" })],
113 ..Default::default()
114 };
115 let parsed = req.constraints_as::<FilesystemAllow>().unwrap();
116 assert_eq!(parsed.len(), 1);
117 assert_eq!(parsed[0].path, "/x/**");
118 }
119
120 #[test]
121 fn description_round_trips_as_bare_string() {
122 let req: CapabilityRequest =
125 serde_json::from_value(serde_json::json!({ "description": "hello" })).unwrap();
126 let v = serde_json::to_value(&req).unwrap();
127 assert_eq!(v, serde_json::json!({ "description": "hello" }));
128 }
129
130 #[test]
131 fn capabilities_cbor_is_map_keyed_by_id() {
132 use crate::cbor;
133 let mut caps = Capabilities::default();
134 caps.0.insert(
135 "wasi:filesystem".into(),
136 CapabilityRequest {
137 constraints: vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })],
138 ..Default::default()
139 },
140 );
141
142 let bytes = cbor::to_cbor(&caps);
143 let back: Capabilities = cbor::from_cbor(&bytes).unwrap();
144
145 assert!(back.has("wasi:filesystem"));
146 assert_eq!(
147 back.get("wasi:filesystem").unwrap().constraints,
148 vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })]
149 );
150 }
151}