use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::Meta;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Hreflang {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LinkObject {
pub href: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub describedby: Option<Box<Link>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hreflang: Option<Hreflang>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Link {
String(String),
Object(LinkObject),
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Links(pub BTreeMap<String, Option<Link>>);
impl Links {
#[must_use]
pub fn new() -> Self {
Self(BTreeMap::new())
}
#[must_use]
pub fn contains(&self, rel: &str) -> bool {
self.0.contains_key(rel)
}
#[must_use]
pub fn get(&self, rel: &str) -> Option<&Link> {
self.0.get(rel).and_then(Option::as_ref)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Link)> + '_ {
self.0
.iter()
.filter_map(|(k, v)| v.as_ref().map(|link| (k.as_str(), link)))
}
pub fn keys(&self) -> impl Iterator<Item = &str> + '_ {
self.0.keys().map(String::as_str)
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_link_string() {
let json = r#""http://example.com/articles/1""#;
let link: Link = serde_json::from_str(json).unwrap();
assert!(matches!(link, Link::String(_)));
assert_eq!(serde_json::to_string(&link).unwrap(), json);
}
#[test]
fn test_link_object() {
let json = r#"{"href":"http://example.com","title":"Example"}"#;
let link: Link = serde_json::from_str(json).unwrap();
match &link {
Link::Object(obj) => {
assert_eq!(obj.href, "http://example.com");
assert_eq!(obj.title.as_deref(), Some("Example"));
}
_ => panic!("expected Link::Object"),
}
}
#[test]
fn test_links_with_null_value() {
let json = r#"{"self":"http://example.com","related":null}"#;
let links: Links = serde_json::from_str(json).unwrap();
assert!(links.0["self"].is_some());
assert!(links.0["related"].is_none());
}
#[test]
fn test_hreflang_single() {
let json = r#""en""#;
let h: Hreflang = serde_json::from_str(json).unwrap();
assert!(matches!(h, Hreflang::Single(ref s) if s == "en"));
assert_eq!(serde_json::to_string(&h).unwrap(), json);
}
#[test]
fn test_hreflang_multiple() {
let json = r#"["en","fr"]"#;
let h: Hreflang = serde_json::from_str(json).unwrap();
assert!(matches!(h, Hreflang::Multiple(ref v) if v.len() == 2));
}
#[test]
fn test_link_object_with_describedby() {
let json = r#"{"href":"http://example.com","describedby":"http://schema.example.com"}"#;
let link: Link = serde_json::from_str(json).unwrap();
match link {
Link::Object(obj) => {
assert!(matches!(obj.describedby.as_deref(), Some(Link::String(_))));
}
_ => panic!("expected Link::Object"),
}
}
fn sample_links() -> Links {
let json = r#"{
"self": "http://example.com/articles/1",
"related": null,
"next": {"href": "http://example.com/articles?page=2", "title": "Next page"}
}"#;
serde_json::from_str(json).unwrap()
}
#[test]
fn links_new_is_empty() {
let links = Links::new();
assert!(links.is_empty());
assert_eq!(links.len(), 0);
}
#[test]
fn links_default_matches_new() {
assert_eq!(Links::default(), Links::new());
}
#[test]
fn links_contains_returns_true_for_null_entry() {
let links = sample_links();
assert!(links.contains("self"));
assert!(links.contains("related"));
assert!(links.contains("next"));
assert!(!links.contains("missing"));
}
#[test]
fn links_get_returns_link_for_present_non_null() {
let links = sample_links();
match links.get("self") {
Some(Link::String(s)) => assert_eq!(s, "http://example.com/articles/1"),
other => panic!("expected Link::String, got {other:?}"),
}
assert!(matches!(links.get("next"), Some(Link::Object(_))));
}
#[test]
fn links_get_returns_none_for_null_entry() {
let links = sample_links();
assert!(links.get("related").is_none());
}
#[test]
fn links_get_returns_none_for_missing_key() {
let links = sample_links();
assert!(links.get("missing").is_none());
}
#[test]
fn links_iter_skips_null_entries() {
let links = sample_links();
let names: Vec<&str> = links.iter().map(|(k, _)| k).collect();
assert_eq!(names, vec!["next", "self"]);
}
#[test]
fn links_keys_includes_null_entries() {
let links = sample_links();
let names: Vec<&str> = links.keys().collect();
assert_eq!(names, vec!["next", "related", "self"]);
}
#[test]
fn links_len_counts_all_entries() {
let links = sample_links();
assert_eq!(links.len(), 3);
assert!(!links.is_empty());
}
}