use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::Error;
use crate::types::jrd_uri::is_absolute_uri;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Rel(String);
impl Rel {
pub fn new<S: AsRef<str>>(rel: S) -> Self {
Self::try_new(rel).expect("invalid WebFinger link relation type")
}
pub fn try_new<S: AsRef<str>>(rel: S) -> Result<Self, Error> {
let rel = rel.as_ref();
if is_absolute_uri(rel) || is_registered_relation_type(rel) {
Ok(Self(rel.to_string()))
} else {
Err(Error::InvalidRel(rel.to_string()))
}
}
}
impl fmt::Display for Rel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for Rel {
type Err = Error;
fn from_str(rel: &str) -> Result<Self, Self::Err> {
Self::try_new(rel)
}
}
impl TryFrom<&str> for Rel {
type Error = Error;
fn try_from(rel: &str) -> Result<Self, Self::Error> {
Self::try_new(rel)
}
}
impl TryFrom<String> for Rel {
type Error = Error;
fn try_from(rel: String) -> Result<Self, Self::Error> {
Self::try_new(rel)
}
}
impl From<Rel> for String {
fn from(rel: Rel) -> Self {
rel.0
}
}
impl AsRef<str> for Rel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for Rel {
fn borrow(&self) -> &str {
&self.0
}
}
impl Serialize for Rel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for Rel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(RelVisitor)
}
}
struct RelVisitor;
impl Visitor<'_> for RelVisitor {
type Value = Rel;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a URI relation type or registered relation type")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Rel::try_new(value).map_err(E::custom)
}
}
fn is_registered_relation_type(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
first.is_ascii_lowercase()
&& chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '-'))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use super::*;
fn assert_common_traits<T>()
where
T: Clone
+ Debug
+ Display
+ Eq
+ Ord
+ Hash
+ Send
+ Sync
+ Serialize
+ for<'de> Deserialize<'de>,
{
}
#[test]
fn implements_applicable_common_traits() {
assert_common_traits::<Rel>();
}
#[test]
fn accepts_uri_relation_types() {
let rel = Rel::try_new("http://webfinger.net/rel/profile-page").unwrap();
assert_eq!(rel.as_ref(), "http://webfinger.net/rel/profile-page");
}
#[test]
fn accepts_registered_relation_types() {
let rel = Rel::try_new("author").unwrap();
assert_eq!(rel.as_ref(), "author");
}
#[test]
fn try_from_parses_valid_relation_types() {
let rel = Rel::try_from("author").unwrap();
assert_eq!(rel.as_ref(), "author");
}
#[test]
fn converts_back_into_owned_string() {
let rel = Rel::new("author");
assert_eq!(String::from(rel), "author");
}
#[test]
fn supports_borrowed_string_set_lookup() {
let mut values = BTreeSet::new();
values.insert(Rel::new("author"));
assert!(values.contains("author"));
}
#[test]
fn orders_by_relation_string() {
let first = Rel::new("author");
let second = Rel::new("http://webfinger.net/rel/profile-page");
assert!(first < second);
}
#[test]
fn rejects_empty_relation_types() {
let error = Rel::try_new("").expect_err("empty relation type");
assert!(error.to_string().contains("invalid relation type"));
}
#[test]
fn rejects_multiple_relation_types_in_one_value() {
let error = Rel::try_new("author avatar").expect_err("multiple relation types");
assert!(error.to_string().contains("invalid relation type"));
}
#[test]
fn rejects_relative_uri_relation_types() {
let error = Rel::try_new("/rel/profile-page").expect_err("relative URI relation type");
assert!(error.to_string().contains("invalid relation type"));
}
#[test]
fn rejects_uri_relation_types_with_malformed_percent_escapes() {
let error = Rel::try_new("http://example.com/a%GG").expect_err("malformed percent escape");
assert!(error.to_string().contains("invalid relation type"));
}
#[test]
fn deserialization_rejects_invalid_relation_types() {
let error = serde_json::from_str::<Rel>(r#""""#).expect_err("empty relation type");
assert!(error.to_string().contains("invalid relation type"));
}
}