ucan_capabilities_object/
lib.rs1use iri_string::types::UriString;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5pub mod ability;
6pub mod nota_bene;
7
8pub use ability::{
9 Ability, AbilityName, AbilityNameRef, AbilityNamespace, AbilityNamespaceRef, AbilityRef,
10};
11pub use nota_bene::NotaBeneCollection;
12
13pub type CapsInner<NB> = BTreeMap<UriString, BTreeMap<Ability, NotaBeneCollection<NB>>>;
14
15#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
17pub struct Capabilities<NB>(CapsInner<NB>);
18
19#[derive(thiserror::Error, Debug)]
20pub enum ConvertError<A, B> {
21 #[error("Failed Conversion: {0}")]
22 A(#[source] A),
23 #[error("Failed Conversion: {0}")]
24 B(#[source] B),
25}
26
27pub type ConvertResult<T, A, B, TA, TB> =
28 Result<T, ConvertError<<TA as TryInto<A>>::Error, <TB as TryInto<B>>::Error>>;
29
30impl<NB> Capabilities<NB> {
31 pub fn new() -> Self {
33 Self(CapsInner::new())
34 }
35
36 pub fn len(&self) -> usize {
37 self.0.len()
38 }
39
40 pub fn is_empty(&self) -> bool {
41 self.0.is_empty()
42 }
43
44 pub fn can<T, A>(
46 &self,
47 target: T,
48 action: A,
49 ) -> ConvertResult<Option<&NotaBeneCollection<NB>>, UriString, Ability, T, A>
50 where
51 T: TryInto<UriString>,
52 A: TryInto<Ability>,
53 {
54 Ok(self.can_do(
55 &target.try_into().map_err(ConvertError::A)?,
56 &action.try_into().map_err(ConvertError::B)?,
57 ))
58 }
59
60 pub fn can_do(&self, target: &UriString, action: &Ability) -> Option<&NotaBeneCollection<NB>> {
62 self.0.get(target).and_then(|m| m.get(action))
63 }
64
65 pub fn merge<NB1, NB2>(self, other: Capabilities<NB1>) -> Capabilities<NB2>
67 where
68 NB2: From<NB1> + From<NB>,
69 {
70 let mut s: Capabilities<NB2> = self.0.into();
71 let o: Capabilities<NB2> = other.0.into();
72
73 for (uri, abs) in o.0.into_iter() {
74 let res_entry = s.0.entry(uri).or_default();
75 for (ab, nbs) in abs.into_iter() {
76 res_entry.entry(ab).or_default().extend(nbs);
77 }
78 }
79 s
80 }
81
82 pub fn merge_convert<NB1, NB2>(
84 self,
85 other: Capabilities<NB1>,
86 ) -> ConvertResult<Capabilities<NB2>, NB2, NB2, NB, NB1>
87 where
88 NB2: TryFrom<NB> + TryFrom<NB1>,
89 {
90 Ok(try_convert(self)
91 .map_err(ConvertError::A)?
92 .merge(try_convert(other).map_err(ConvertError::B)?))
93 }
94
95 pub fn with_action(
97 &mut self,
98 target: UriString,
99 action: Ability,
100 nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
101 ) -> &mut Self {
102 self.0
103 .entry(target)
104 .or_default()
105 .entry(action)
106 .or_default()
107 .extend(nb);
108 self
109 }
110
111 pub fn with_action_convert<T, A>(
115 &mut self,
116 target: T,
117 action: A,
118 nb: impl IntoIterator<Item = BTreeMap<String, NB>>,
119 ) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
120 where
121 T: TryInto<UriString>,
122 A: TryInto<Ability>,
123 {
124 Ok(self.with_action(
125 target.try_into().map_err(ConvertError::A)?,
126 action.try_into().map_err(ConvertError::B)?,
127 nb,
128 ))
129 }
130
131 pub fn with_actions(
133 &mut self,
134 target: UriString,
135 abilities: impl IntoIterator<Item = (Ability, impl IntoIterator<Item = BTreeMap<String, NB>>)>,
136 ) -> &mut Self {
137 let entry = self.0.entry(target).or_default();
138 for (ability, nbs) in abilities {
139 let ab_entry = entry.entry(ability).or_default();
140 ab_entry.extend(nbs);
141 }
142 self
143 }
144
145 pub fn with_actions_convert<T, A, N>(
149 &mut self,
150 target: T,
151 abilities: impl IntoIterator<Item = (A, N)>,
152 ) -> Result<&mut Self, ConvertError<T::Error, A::Error>>
153 where
154 T: TryInto<UriString>,
155 A: TryInto<Ability>,
156 N: IntoIterator<Item = BTreeMap<String, NB>>,
157 {
158 Ok(self.with_actions(
159 target.try_into().map_err(ConvertError::A)?,
160 abilities
161 .into_iter()
162 .map(|(a, n)| Ok((a.try_into()?, n)))
163 .collect::<Result<Vec<(Ability, N)>, A::Error>>()
164 .map_err(ConvertError::B)?,
165 ))
166 }
167
168 pub fn abilities(&self) -> &CapsInner<NB> {
170 &self.0
171 }
172
173 pub fn abilities_for<T>(
175 &self,
176 target: T,
177 ) -> Result<Option<&BTreeMap<Ability, NotaBeneCollection<NB>>>, T::Error>
178 where
179 T: TryInto<UriString>,
180 {
181 Ok(self.0.get(&target.try_into()?))
182 }
183
184 pub fn into_inner(self) -> CapsInner<NB> {
185 self.0
186 }
187}
188
189impl<NB1, NB2> From<CapsInner<NB1>> for Capabilities<NB2>
190where
191 NB2: From<NB1>,
192{
193 fn from(attenuations: CapsInner<NB1>) -> Self {
194 Self(
195 attenuations
196 .into_iter()
197 .map(|(uri, abilities)| {
198 (
199 uri,
200 abilities
201 .into_iter()
202 .map(|(ability, nbs)| (ability, nbs.into_inner().into()))
203 .collect(),
204 )
205 })
206 .collect(),
207 )
208 }
209}
210
211pub fn try_convert<NB1, NB2>(caps: Capabilities<NB1>) -> Result<Capabilities<NB2>, NB2::Error>
212where
213 NB2: TryFrom<NB1>,
214{
215 Ok(Capabilities(
216 caps.0
217 .into_iter()
218 .map(|(uri, abilities)| {
219 Ok((
220 uri,
221 abilities
222 .into_iter()
223 .map(|(ability, nbs)| Ok((ability, nota_bene::try_convert(nbs)?)))
224 .collect::<Result<BTreeMap<Ability, NotaBeneCollection<NB2>>, NB2::Error>>(
225 )?,
226 ))
227 })
228 .collect::<Result<CapsInner<NB2>, NB2::Error>>()?,
229 ))
230}
231
232impl<NB> Default for Capabilities<NB> {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn insertion() {
244 let mut caps = Capabilities::<String>::new();
245 assert_eq!(caps.can("https://example.com", "crud/read").unwrap(), None);
246 caps.with_action_convert("https://example.com", "crud/read", [])
247 .unwrap();
248 assert_eq!(
249 caps.can("https://example.com", "crud/read")
250 .unwrap()
251 .unwrap(),
252 &NotaBeneCollection::<String>::new()
253 );
254 }
255
256 #[test]
257 fn merging() {
258 let mut caps1 = Capabilities::<String>::new();
259 caps1
260 .with_action_convert(
261 "https://example.com",
262 "crud/read",
263 [[("foo".into(), "bar".into())].into_iter().collect()],
264 )
265 .unwrap();
266
267 let mut caps2 = Capabilities::<String>::new();
268 caps2
269 .with_action_convert("https://example.com", "crud/update", [])
270 .unwrap()
271 .with_action_convert("https://another.com", "crud/read", [])
272 .unwrap();
273
274 let mut caps_merged = Capabilities::<String>::new();
275 caps_merged
276 .with_action_convert(
277 "https://example.com",
278 "crud/read",
279 [[("foo".into(), "bar".into())].into_iter().collect()],
280 )
281 .unwrap()
282 .with_action_convert("https://example.com", "crud/update", [])
283 .unwrap()
284 .with_action_convert("https://another.com", "crud/read", [])
285 .unwrap();
286
287 assert_eq!(caps1.merge(caps2), caps_merged);
288 }
289
290 #[test]
291 fn serde() {
292 let mut caps = Capabilities::<String>::new();
293 assert_eq!(serde_json::to_string(&caps).unwrap(), r#"{}"#);
294
295 let with_one = r#"{"https://example.com/":{"crud/read":[{}]}}"#;
296
297 caps.with_action_convert("https://example.com/", "crud/read", [])
298 .unwrap();
299 assert_eq!(serde_json::to_string(&caps).unwrap(), with_one);
300 assert_eq!(
301 serde_json::from_str::<Capabilities<String>>(with_one).unwrap(),
302 caps
303 );
304
305 caps.with_action_convert(
306 "https://example.com/",
307 "crud/read",
308 [[("foo".into(), "bar".into())].into_iter().collect()],
309 )
310 .unwrap();
311
312 let with_two = r#"{"https://example.com/":{"crud/read":[{"foo":"bar"}]}}"#;
313 assert_eq!(serde_json::to_string(&caps).unwrap(), with_two);
314 assert_eq!(
315 serde_json::from_str::<Capabilities<String>>(with_two).unwrap(),
316 caps
317 );
318
319 let with_three = r#"{"https://another.com":{"crud/update":[{"bar":"baz"}]},"https://example.com/":{"crud/read":[{"foo":"bar"}]}}"#;
320 caps.with_action_convert(
321 "https://another.com",
322 "crud/update",
323 [[("bar".into(), "baz".into())].into_iter().collect()],
324 )
325 .unwrap();
326 assert_eq!(serde_json::to_string(&caps).unwrap(), with_three);
327 assert_eq!(
328 serde_json::from_str::<Capabilities<String>>(with_three).unwrap(),
329 caps
330 );
331 }
332}