use std::collections::BTreeMap;
use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::{JrdUri, Rel};
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Link {
pub rel: Rel,
pub r#type: Option<String>,
pub href: Option<JrdUri>,
pub template: Option<String>,
pub titles: Option<BTreeMap<String, String>>,
pub properties: Option<BTreeMap<JrdUri, Option<String>>>,
}
impl Link {
pub fn new(rel: Rel) -> Self {
Self {
rel,
r#type: None,
href: None,
template: None,
titles: None,
properties: None,
}
}
pub fn builder<R: AsRef<str>>(rel: R) -> LinkBuilder {
LinkBuilder::new(rel)
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LinkBuilder {
link: Link,
}
impl LinkBuilder {
pub fn new<R: AsRef<str>>(rel: R) -> Self {
Self {
link: Link::new(Rel::new(rel)),
}
}
pub fn r#type<S: Into<String>>(mut self, r#type: S) -> Self {
self.link.r#type = Some(r#type.into());
self
}
pub fn href<S: AsRef<str>>(mut self, href: S) -> Self {
self.link.href = Some(JrdUri::new(href));
self
}
pub fn template<S: Into<String>>(mut self, template: S) -> Self {
self.link.template = Some(template.into());
self
}
pub fn title<L: Into<String>, V: Into<String>>(mut self, language: L, value: V) -> Self {
let title = Title::new(language, value);
self.link
.titles
.get_or_insert_with(BTreeMap::new)
.insert(title.language, title.value);
self
}
pub fn titles<I, L, V>(mut self, titles: I) -> Self
where
I: IntoIterator<Item = (L, V)>,
L: Into<String>,
V: Into<String>,
{
let titles = titles
.into_iter()
.map(|(language, value)| (language.into(), value.into()))
.collect();
self.link.titles = Some(titles);
self
}
pub fn property<K: AsRef<str>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.link
.properties
.get_or_insert_with(BTreeMap::new)
.insert(JrdUri::new(key), Some(value.into()));
self
}
pub fn null_property<K: AsRef<str>>(mut self, key: K) -> Self {
self.link
.properties
.get_or_insert_with(BTreeMap::new)
.insert(JrdUri::new(key), None);
self
}
pub fn properties<I>(mut self, properties: I) -> Self
where
I: IntoIterator<Item = (JrdUri, Option<String>)>,
{
self.link.properties = Some(properties.into_iter().collect());
self
}
pub fn build(self) -> Link {
self.link
}
}
impl From<LinkBuilder> for Link {
fn from(builder: LinkBuilder) -> Self {
builder.build()
}
}
impl Debug for LinkBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("LinkBuilder").field(&self.link).finish()
}
}
impl Debug for Link {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Link");
let mut debug = debug.field("rel", &self.rel);
if let Some(r#type) = &self.r#type {
debug = debug.field("type", &r#type);
}
if let Some(href) = &self.href {
debug = debug.field("href", &href);
}
if let Some(template) = &self.template {
debug = debug.field("template", &template);
}
if let Some(titles) = &self.titles {
debug = debug.field("titles", &titles);
}
if let Some(properties) = &self.properties {
debug = debug.field("properties", &properties);
}
debug.finish()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Title {
pub language: String,
pub value: String,
}
impl Title {
pub fn new<L: Into<String>, V: Into<String>>(language: L, value: V) -> Self {
Self {
language: language.into(),
value: value.into(),
}
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::*;
type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn assert_data_traits<T>()
where
T: Clone + Debug + Eq + Ord + Hash + Send + Sync + Serialize + for<'de> Deserialize<'de>,
{
}
fn assert_ordered_value_traits<T>()
where
T: Clone + Debug + Eq + Ord + Hash + Send + Sync + Serialize + for<'de> Deserialize<'de>,
{
}
fn assert_builder_traits<T>()
where
T: Clone + Debug + Eq + Ord + Hash + Send + Sync,
{
}
#[test]
fn implements_applicable_common_traits() {
assert_data_traits::<Link>();
assert_ordered_value_traits::<Title>();
assert_builder_traits::<LinkBuilder>();
}
#[test]
fn builder_serializes_titles_as_language_object() -> Result {
let link = Link::builder("http://webfinger.net/rel/profile-page")
.href("https://example.com/profile/carol")
.title("en-us", "Carol's Profile")
.build();
let json = serde_json::to_value(link)?;
assert_eq!(
json,
json!({
"rel": "http://webfinger.net/rel/profile-page",
"href": "https://example.com/profile/carol",
"titles": {
"en-us": "Carol's Profile"
}
})
);
Ok(())
}
#[test]
fn builder_serializes_template() -> Result {
let link = Link::builder("http://ostatus.org/schema/1.0/subscribe")
.template("https://example.com/authorize_interaction?uri={uri}")
.build();
let json = serde_json::to_value(link)?;
assert_eq!(
json,
json!({
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://example.com/authorize_interaction?uri={uri}",
})
);
Ok(())
}
#[test]
fn deserializes_template() -> Result {
let json = r#"
{
"rel": "copyright",
"template": "http://example.com/copyright?id={uri}"
}
"#;
let link: Link = serde_json::from_str(json)?;
assert_eq!(link.rel.as_ref(), "copyright");
assert_eq!(
link.template.as_deref(),
Some("http://example.com/copyright?id={uri}")
);
Ok(())
}
#[test]
fn builder_serializes_null_properties() -> Result {
let link = Link::builder("author")
.property("https://example.com/ns/role", "editor")
.null_property("https://example.com/ns/old-role")
.build();
let json = serde_json::to_value(link)?;
assert_eq!(
json,
json!({
"rel": "author",
"properties": {
"https://example.com/ns/role": "editor",
"https://example.com/ns/old-role": null
}
})
);
Ok(())
}
#[test]
fn deserialization_rejects_title_array_shape() {
let json = r#"
{
"rel": "author",
"titles": [
{
"language": "en-us",
"value": "Carol"
}
]
}
"#;
let error = serde_json::from_str::<Link>(json).expect_err("title array");
assert!(error.to_string().contains("invalid type"));
}
#[test]
fn deserialization_rejects_relative_property_identifiers() {
let json = r#"
{
"rel": "author",
"properties": {
"/ns/role": "editor"
}
}
"#;
let error = serde_json::from_str::<Link>(json).expect_err("relative property identifier");
assert!(error.to_string().contains("invalid JRD URI"));
}
}