Skip to main content

webfinger_rs/types/
rel.rs

1use std::borrow::Borrow;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::Error;
9use crate::types::jrd_uri::is_absolute_uri;
10
11/// Link relation type.
12///
13/// WebFinger relation types are either absolute URI strings or IANA-registered relation type
14/// names. RFC 7033 requires each `rel` member to contain exactly one relation type, so this type
15/// rejects empty strings, relative URI references, and strings that try to carry multiple
16/// relation types.
17///
18/// `Rel` serializes as a JSON string and implements [`AsRef<str>`] for comparison or lookup
19/// without allocating. Builder methods accept strings for common use, but store this validated type
20/// so deserialized and programmatically built links use the same representation.
21///
22/// Registered relation type names use the `reg-rel-type` syntax from [RFC 5988 section 5.3].
23/// URI-valued relation types are validated as absolute URI strings under RFC 3986, including the
24/// [section 2.1] percent-encoding rule.
25///
26/// See [RFC 7033 section 4.4.4.1].
27///
28/// # Examples
29///
30/// ```rust
31/// use webfinger_rs::Rel;
32///
33/// let registered = Rel::try_new("author")?;
34/// let uri = Rel::try_new("http://webfinger.net/rel/profile-page")?;
35///
36/// assert_eq!(registered.as_ref(), "author");
37/// assert_eq!(uri.as_ref(), "http://webfinger.net/rel/profile-page");
38/// # Ok::<(), webfinger_rs::Error>(())
39/// ```
40///
41/// Multiple relation types belong in multiple request `rel` parameters or multiple links, not in
42/// one `Rel` value:
43///
44/// ```rust
45/// use webfinger_rs::Rel;
46///
47/// assert!(Rel::try_new("author avatar").is_err());
48/// ```
49///
50/// [RFC 7033 section 4.4.4.1]: https://www.rfc-editor.org/rfc/rfc7033.html#section-4.4.4.1
51/// [RFC 5988 section 5.3]: https://www.rfc-editor.org/rfc/rfc5988.html#section-5.3
52/// [section 2.1]: https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1
53#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct Rel(String);
55
56impl Rel {
57    /// Creates a link relation type.
58    ///
59    /// This constructor is convenient for application-controlled relation strings. Use
60    /// [`Rel::try_new`] when parsing external input.
61    ///
62    /// # Panics
63    ///
64    /// Panics if `rel` is not a URI relation type or registered relation type name. Use
65    /// [`Rel::try_new`] when handling untrusted input.
66    pub fn new<S: AsRef<str>>(rel: S) -> Self {
67        Self::try_new(rel).expect("invalid WebFinger link relation type")
68    }
69
70    /// Tries to create a link relation type.
71    ///
72    /// URI relation types must be absolute URI strings. Registered relation type names follow the
73    /// `reg-rel-type` syntax from RFC 5988: a lowercase ASCII letter followed by lowercase ASCII
74    /// letters, digits, `.`, or `-`.
75    pub fn try_new<S: AsRef<str>>(rel: S) -> Result<Self, Error> {
76        let rel = rel.as_ref();
77        if is_absolute_uri(rel) || is_registered_relation_type(rel) {
78            Ok(Self(rel.to_string()))
79        } else {
80            Err(Error::InvalidRel(rel.to_string()))
81        }
82    }
83}
84
85impl fmt::Display for Rel {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(&self.0)
88    }
89}
90
91impl FromStr for Rel {
92    type Err = Error;
93
94    fn from_str(rel: &str) -> Result<Self, Self::Err> {
95        Self::try_new(rel)
96    }
97}
98
99impl TryFrom<&str> for Rel {
100    type Error = Error;
101
102    fn try_from(rel: &str) -> Result<Self, Self::Error> {
103        Self::try_new(rel)
104    }
105}
106
107impl TryFrom<String> for Rel {
108    type Error = Error;
109
110    fn try_from(rel: String) -> Result<Self, Self::Error> {
111        Self::try_new(rel)
112    }
113}
114
115impl From<Rel> for String {
116    fn from(rel: Rel) -> Self {
117        rel.0
118    }
119}
120
121impl AsRef<str> for Rel {
122    fn as_ref(&self) -> &str {
123        &self.0
124    }
125}
126
127impl Borrow<str> for Rel {
128    fn borrow(&self) -> &str {
129        &self.0
130    }
131}
132
133impl Serialize for Rel {
134    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135    where
136        S: Serializer,
137    {
138        serializer.serialize_str(&self.0)
139    }
140}
141
142impl<'de> Deserialize<'de> for Rel {
143    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144    where
145        D: Deserializer<'de>,
146    {
147        deserializer.deserialize_str(RelVisitor)
148    }
149}
150
151struct RelVisitor;
152
153impl Visitor<'_> for RelVisitor {
154    type Value = Rel;
155
156    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        formatter.write_str("a URI relation type or registered relation type")
158    }
159
160    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
161    where
162        E: de::Error,
163    {
164        Rel::try_new(value).map_err(E::custom)
165    }
166}
167
168fn is_registered_relation_type(value: &str) -> bool {
169    let mut chars = value.chars();
170    let Some(first) = chars.next() else {
171        return false;
172    };
173    first.is_ascii_lowercase()
174        && chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '-'))
175}
176
177#[cfg(test)]
178mod tests {
179    use std::collections::BTreeSet;
180    use std::fmt::{Debug, Display};
181    use std::hash::Hash;
182
183    use serde::{Deserialize, Serialize};
184
185    use super::*;
186
187    fn assert_common_traits<T>()
188    where
189        T: Clone
190            + Debug
191            + Display
192            + Eq
193            + Ord
194            + Hash
195            + Send
196            + Sync
197            + Serialize
198            + for<'de> Deserialize<'de>,
199    {
200    }
201
202    #[test]
203    fn implements_applicable_common_traits() {
204        assert_common_traits::<Rel>();
205    }
206
207    #[test]
208    fn accepts_uri_relation_types() {
209        let rel = Rel::try_new("http://webfinger.net/rel/profile-page").unwrap();
210
211        assert_eq!(rel.as_ref(), "http://webfinger.net/rel/profile-page");
212    }
213
214    #[test]
215    fn accepts_registered_relation_types() {
216        let rel = Rel::try_new("author").unwrap();
217
218        assert_eq!(rel.as_ref(), "author");
219    }
220
221    #[test]
222    fn try_from_parses_valid_relation_types() {
223        let rel = Rel::try_from("author").unwrap();
224
225        assert_eq!(rel.as_ref(), "author");
226    }
227
228    #[test]
229    fn converts_back_into_owned_string() {
230        let rel = Rel::new("author");
231
232        assert_eq!(String::from(rel), "author");
233    }
234
235    #[test]
236    fn supports_borrowed_string_set_lookup() {
237        let mut values = BTreeSet::new();
238        values.insert(Rel::new("author"));
239
240        assert!(values.contains("author"));
241    }
242
243    #[test]
244    fn orders_by_relation_string() {
245        let first = Rel::new("author");
246        let second = Rel::new("http://webfinger.net/rel/profile-page");
247
248        assert!(first < second);
249    }
250
251    #[test]
252    fn rejects_empty_relation_types() {
253        let error = Rel::try_new("").expect_err("empty relation type");
254
255        assert!(error.to_string().contains("invalid relation type"));
256    }
257
258    #[test]
259    fn rejects_multiple_relation_types_in_one_value() {
260        let error = Rel::try_new("author avatar").expect_err("multiple relation types");
261
262        assert!(error.to_string().contains("invalid relation type"));
263    }
264
265    #[test]
266    fn rejects_relative_uri_relation_types() {
267        let error = Rel::try_new("/rel/profile-page").expect_err("relative URI relation type");
268
269        assert!(error.to_string().contains("invalid relation type"));
270    }
271
272    #[test]
273    fn rejects_uri_relation_types_with_malformed_percent_escapes() {
274        let error = Rel::try_new("http://example.com/a%GG").expect_err("malformed percent escape");
275
276        assert!(error.to_string().contains("invalid relation type"));
277    }
278
279    #[test]
280    fn deserialization_rejects_invalid_relation_types() {
281        let error = serde_json::from_str::<Rel>(r#""""#).expect_err("empty relation type");
282
283        assert!(error.to_string().contains("invalid relation type"));
284    }
285}