ucan_capabilities_object/
lib.rs

1use 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/// Representation of a set of delegated Capabilities.
16#[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    /// Create a new empty set of Capabilities.
32    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    /// Check if a particular action is allowed for the specified target, or is allowed globally.
45    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    /// Check if a particular action is allowed for the specified target, or is allowed globally, without type conversion.
61    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    /// Merge this Capabilities set with another
66    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    /// Merge this Capabilities set with another, where the two sets have different Nota Bene types.
83    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    /// Add an allowed action for the given target, with a set of note-benes
96    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    /// Add an allowed action for the given target, with a set of note-benes.
112    ///
113    /// This method automatically converts the provided args into the correct types for convenience.
114    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    /// Add a set of allowed action for the given target, with associated note-benes
132    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    /// Add a set of allowed action for the given target, with associated note-benes.
146    ///
147    /// This method automatically converts the provided args into the correct types for convenience.
148    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    /// Read the set of abilities granted in this capabilities set
169    pub fn abilities(&self) -> &CapsInner<NB> {
170        &self.0
171    }
172
173    /// Read the set of abilities granted for a given target in this capabilities set
174    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}