ucan_capabilities_object/
ability.rs

1use nutype::nutype;
2use serde::{Deserialize, Serialize};
3use std::fmt::{Display, Error as FmtError, Formatter};
4
5#[nutype(validate(with = is_valid_ability))]
6#[derive(*, Display, TryFrom, Into, Borrow, Serialize, Deserialize)]
7pub struct Ability(String);
8
9#[nutype(validate(with = is_valid))]
10#[derive(*, Display, TryFrom, Into, Borrow, Serialize, Deserialize)]
11pub struct AbilityNamespace(String);
12
13#[nutype(validate(with = is_valid))]
14#[derive(*, Display, TryFrom, Into, Borrow, Serialize, Deserialize)]
15pub struct AbilityName(String);
16
17#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, PartialOrd, Ord)]
18pub struct AbilityRef<'a>(&'a str);
19
20#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, PartialOrd, Ord)]
21pub struct AbilityNamespaceRef<'a>(&'a str);
22
23#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, PartialOrd, Ord)]
24pub struct AbilityNameRef<'a>(&'a str);
25
26impl Ability {
27    pub fn from_parts(namespace: AbilityNamespace, name: AbilityName) -> Self {
28        // these are already validated, so this is safe
29        Self::new([namespace.as_ref(), name.as_ref()].join("/")).unwrap()
30    }
31    pub fn get_ref(&self) -> AbilityRef {
32        AbilityRef(self.as_ref())
33    }
34    pub fn into_parts(self) -> (AbilityNamespace, AbilityName) {
35        let split = self.as_ref().split_once('/').unwrap();
36        // these are already validated, so this is safe
37        let namespace = AbilityNamespace::new(split.0.to_string()).unwrap();
38        let name = AbilityName::new(split.1.to_string()).unwrap();
39
40        (namespace, name)
41    }
42    pub fn namespace(&self) -> AbilityNamespaceRef {
43        AbilityNamespaceRef(self.as_ref().split_once('/').unwrap().0)
44    }
45    pub fn name(&self) -> AbilityNameRef {
46        AbilityNameRef(self.as_ref().split_once('/').unwrap().1)
47    }
48    pub fn len(&self) -> usize {
49        self.as_ref().len()
50    }
51    pub fn is_empty(&self) -> bool {
52        self.as_ref().is_empty()
53    }
54}
55
56impl<'a> AbilityNamespaceRef<'a> {
57    pub fn to_owned(&self) -> AbilityNamespace {
58        AbilityNamespace::try_from(self.as_ref()).unwrap()
59    }
60}
61
62impl<'a> AbilityNameRef<'a> {
63    pub fn to_owned(&self) -> AbilityName {
64        AbilityName::try_from(self.as_ref()).unwrap()
65    }
66}
67
68impl<'a> AbilityRef<'a> {
69    pub fn to_owned(&self) -> Ability {
70        Ability::try_from(self.as_ref()).unwrap()
71    }
72    pub fn namespace(&self) -> AbilityNamespaceRef {
73        AbilityNamespaceRef(self.0.split_once('/').unwrap().0)
74    }
75
76    pub fn name(&self) -> AbilityNameRef {
77        AbilityNameRef(self.0.split_once('/').unwrap().1)
78    }
79
80    pub fn len(&self) -> usize {
81        self.0.len()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.0.is_empty()
86    }
87}
88
89impl<'a> Display for AbilityRef<'a> {
90    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
91        write!(f, "{}", &self.0)
92    }
93}
94
95impl<'a> Display for AbilityNamespaceRef<'a> {
96    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
97        write!(f, "{}", &self.0)
98    }
99}
100
101impl<'a> Display for AbilityNameRef<'a> {
102    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
103        write!(f, "{}", &self.0)
104    }
105}
106
107impl AsRef<str> for AbilityRef<'_> {
108    fn as_ref(&self) -> &str {
109        self.0
110    }
111}
112
113impl AsRef<str> for AbilityNameRef<'_> {
114    fn as_ref(&self) -> &str {
115        self.0
116    }
117}
118
119impl AsRef<str> for AbilityNamespaceRef<'_> {
120    fn as_ref(&self) -> &str {
121        self.0
122    }
123}
124
125const ALLOWED_CHARS: &str = "-_.+*";
126
127fn not_allowed(c: char) -> bool {
128    !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c)
129}
130
131fn is_valid(s: &str) -> bool {
132    !s.is_empty() && !s.contains(not_allowed)
133}
134
135fn is_valid_ability(s: &str) -> bool {
136    s.split_once('/')
137        .map(|(namespace, name)| is_valid(namespace) && is_valid(name))
138        .unwrap_or(false)
139}
140
141impl<'a> TryFrom<&'a str> for AbilityRef<'a> {
142    type Error = <Ability as TryFrom<&'a str>>::Error;
143    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
144        if is_valid_ability(s) {
145            Ok(Self(s))
146        } else {
147            Err(Self::Error::Invalid)
148        }
149    }
150}
151
152impl<'a> TryFrom<&'a str> for AbilityNamespaceRef<'a> {
153    type Error = <AbilityNamespace as TryFrom<&'a str>>::Error;
154    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
155        if is_valid(s) {
156            Ok(Self(s))
157        } else {
158            Err(Self::Error::Invalid)
159        }
160    }
161}
162
163impl<'a> TryFrom<&'a str> for AbilityNameRef<'a> {
164    type Error = <AbilityName as TryFrom<&'a str>>::Error;
165    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
166        if is_valid(s) {
167            Ok(Self(s))
168        } else {
169            Err(Self::Error::Invalid)
170        }
171    }
172}
173
174#[cfg(test)]
175mod test {
176    use super::*;
177
178    #[test]
179    fn invalid_parts() {
180        let invalids = [
181            "https://example.com/",
182            "-my-namespace:",
183            "my-namespace-/",
184            "my--namespace[]",
185            "not a valid namespace",
186            "",
187        ];
188        for s in invalids {
189            s.parse::<AbilityNamespace>().unwrap_err();
190            s.parse::<AbilityName>().unwrap_err();
191        }
192    }
193
194    #[test]
195    fn valid_parts() {
196        let valids = ["my-namespace", "My-nAmespac3-2", "*"];
197        for s in valids {
198            s.parse::<AbilityNamespace>().unwrap();
199            s.parse::<AbilityName>().unwrap();
200        }
201    }
202
203    #[test]
204    fn valid_abilities() {
205        for s in [
206            "credential/present",
207            "kv/list",
208            "some-ns/some-name",
209            "msg/*",
210        ] {
211            let ab = s.parse::<Ability>().unwrap();
212            let (ns, n) = s.split_once('/').unwrap();
213            assert_eq!(ab.namespace().as_ref(), ns);
214            assert_eq!(ab.name().as_ref(), n);
215
216            let (namespace, name) = ab.clone().into_parts();
217            assert_eq!(namespace.as_ref(), ns);
218            assert_eq!(name.as_ref(), n);
219
220            let ability = Ability::from_parts(namespace, name);
221            assert_eq!(ability, ab);
222            assert_eq!(ability.as_ref(), s);
223        }
224    }
225
226    #[test]
227    fn invalid_abilities() {
228        for s in [
229            "credential ns/present",
230            "kv-list",
231            "some:ns/some-name",
232            "msg/wrong/str",
233            "/",
234            "//",
235            "over/one/slash",
236        ] {
237            s.parse::<Ability>().unwrap_err();
238        }
239    }
240
241    #[test]
242    fn ordering() {
243        let abilities: Vec<Ability> = [
244            "a/b", "a/c", "aa/a", "b/a", "kv*/read", "kv/list", "kv/read", "kva/get",
245        ]
246        .into_iter()
247        .map(|s| s.parse().unwrap())
248        .collect();
249
250        let mut sorted = abilities.clone();
251        sorted.sort();
252
253        assert_eq!(sorted, abilities);
254    }
255
256    #[test]
257    fn serde() {
258        use serde_json::{from_str, to_string};
259
260        let ab_str = r#""credential/present""#;
261
262        let ability: Ability = from_str(ab_str).unwrap();
263
264        #[derive(Serialize, Deserialize, Debug, PartialEq)]
265        struct Wrapper(Ability);
266
267        let wrapped: Wrapper = from_str(ab_str).unwrap();
268
269        assert_eq!(wrapped.0, ability);
270        assert_eq!(to_string(&wrapped).unwrap(), ab_str);
271    }
272}