ucan_capabilities_object/
ability.rs1use 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 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 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}