use std::collections::BTreeMap;
use std::fmt::{self, Debug};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::Error;
use crate::{JrdUri, Link};
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Response {
pub subject: JrdUri,
pub aliases: Option<Vec<JrdUri>>,
pub properties: Option<BTreeMap<JrdUri, Option<String>>>,
#[serde(default)]
pub links: Vec<Link>,
}
impl Response {
pub fn new<S: AsRef<str>>(subject: S) -> Self {
Self {
subject: JrdUri::new(subject),
aliases: None,
properties: None,
links: Vec::new(),
}
}
pub fn builder<S: AsRef<str>>(subject: S) -> Builder {
Builder::new(subject)
}
pub fn try_builder<S: AsRef<str>>(subject: S) -> Result<Builder, Error> {
Ok(Builder::new(JrdUri::try_new(subject)?))
}
}
impl fmt::Display for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Builder {
response: Response,
}
impl Builder {
pub fn new<S: AsRef<str>>(subject: S) -> Self {
Self {
response: Response::new(subject),
}
}
pub fn alias<S: AsRef<str>>(mut self, alias: S) -> Self {
self.response
.aliases
.get_or_insert_with(Vec::new)
.push(JrdUri::new(alias));
self
}
pub fn property<K: AsRef<str>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.response
.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.response
.properties
.get_or_insert_with(BTreeMap::new)
.insert(JrdUri::new(key), None);
self
}
pub fn link<L: Into<Link>>(mut self, link: L) -> Self {
self.response.links.push(link.into());
self
}
pub fn links(mut self, links: Vec<Link>) -> Self {
self.response.links = links;
self
}
pub fn build(self) -> Response {
self.response
}
}
impl Debug for Builder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Builder").field(&self.response).finish()
}
}
impl Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Response");
let mut debug = debug.field("subject", &self.subject);
if let Some(aliases) = &self.aliases {
debug = debug.field("aliases", &aliases);
}
if let Some(properties) = &self.properties {
debug = debug.field("properties", &properties);
}
debug.field("links", &self.links).finish()
}
}
#[cfg(test)]
mod tests {
use std::fmt::{Debug, Display};
use std::hash::Hash;
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::*;
use crate::Rel;
type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn assert_data_traits<T>()
where
T: Clone
+ Debug
+ Display
+ 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::<Response>();
assert_builder_traits::<Builder>();
}
#[test]
fn deserializes_rfc_shaped_jrd_with_null_properties_and_title_object() -> Result {
let json = r#"
{
"subject": "http://blog.example.com/article/id/314",
"aliases": [
"http://blog.example.com/cool_new_thing",
"http://blog.example.com/steve/article/7"
],
"properties": {
"http://blgx.example.net/ns/version": "1.3",
"http://blgx.example.net/ns/ext": null
},
"links": [
{
"rel": "author",
"href": "http://blog.example.com/author/steve",
"titles": {
"en-us": "The Magical World of Steve",
"fr": "Le Monde Magique de Steve"
},
"properties": {
"http://example.com/role": "editor",
"http://example.com/old-role": null
}
}
]
}
"#;
let response = serde_json::from_str::<Response>(json)?;
let properties = response.properties.as_ref().expect("properties");
let links = &response.links;
let link = links.first().expect("link");
let titles = link.titles.as_ref().expect("titles");
let link_properties = link.properties.as_ref().expect("link properties");
assert_eq!(
response.subject.as_ref(),
"http://blog.example.com/article/id/314"
);
assert_eq!(
response.aliases.as_ref().expect("aliases")[0].as_ref(),
"http://blog.example.com/cool_new_thing"
);
assert_eq!(
properties
.get(&JrdUri::new("http://blgx.example.net/ns/version"))
.expect("version")
.as_deref(),
Some("1.3")
);
assert_eq!(
properties.get(&JrdUri::new("http://blgx.example.net/ns/ext")),
Some(&None)
);
assert_eq!(link.rel, Rel::new("author"));
assert_eq!(
link.href.as_ref().expect("href").as_ref(),
"http://blog.example.com/author/steve"
);
assert_eq!(
titles.get("en-us").map(String::as_str),
Some("The Magical World of Steve")
);
assert_eq!(
link_properties.get(&JrdUri::new("http://example.com/old-role")),
Some(&None)
);
Ok(())
}
#[test]
fn serializes_builder_output_as_rfc_shaped_jrd() -> Result {
let response = Response::builder("acct:carol@example.com")
.alias("https://example.com/profile/carol")
.property("https://example.com/ns/role", "developer")
.null_property("https://example.com/ns/old-role")
.link(
Link::builder("http://webfinger.net/rel/profile-page")
.href("https://example.com/profile/carol")
.title("en-us", "Carol's Profile")
.property("https://example.com/ns/verified", "true")
.null_property("https://example.com/ns/legacy"),
)
.build();
let json = serde_json::to_value(response)?;
assert_eq!(
json,
json!({
"subject": "acct:carol@example.com",
"aliases": ["https://example.com/profile/carol"],
"properties": {
"https://example.com/ns/role": "developer",
"https://example.com/ns/old-role": null
},
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"href": "https://example.com/profile/carol",
"titles": {
"en-us": "Carol's Profile"
},
"properties": {
"https://example.com/ns/verified": "true",
"https://example.com/ns/legacy": null
}
}
]
})
);
Ok(())
}
#[test]
fn rejects_relative_jrd_uris() {
let json =
r#"{"subject":"acct:carol@example.com","links":[{"rel":"author","href":"/carol"}]}"#;
let error = serde_json::from_str::<Response>(json).expect_err("relative href");
assert!(error.to_string().contains("invalid JRD URI"));
}
#[test]
fn rejects_empty_relation_types() {
let json = r#"{"subject":"acct:carol@example.com","links":[{"rel":""}]}"#;
let error = serde_json::from_str::<Response>(json).expect_err("empty rel");
assert!(error.to_string().contains("invalid relation type"));
}
#[test]
fn deserializes_jrd_without_links() -> Result {
let json = r#"{"subject":"acct:carol@example.com"}"#;
let response = serde_json::from_str::<Response>(json)?;
assert!(response.links.is_empty());
Ok(())
}
}