actpub_activitystreams/
link.rs1use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13use url::Url;
14
15use crate::kind;
16use crate::value::{HasId, OneOrMany};
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct Link {
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub id: Option<Url>,
38
39 #[serde(rename = "type", default = "Link::default_kind")]
41 pub kind: OneOrMany<String>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub href: Option<Url>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub rel: Option<OneOrMany<String>>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub media_type: Option<String>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub name: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub name_map: Option<BTreeMap<String, String>>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub hreflang: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub height: Option<u64>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub width: Option<u64>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub preview: Option<Box<crate::object::ObjectRef>>,
78
79 #[serde(flatten)]
82 pub extra: BTreeMap<String, serde_json::Value>,
83}
84
85impl Link {
86 fn default_kind() -> OneOrMany<String> {
87 OneOrMany::one(kind::core::LINK.to_owned())
88 }
89
90 #[must_use]
92 pub fn new(href: Url) -> Self {
93 Self {
94 id: None,
95 kind: Self::default_kind(),
96 href: Some(href),
97 rel: None,
98 media_type: None,
99 name: None,
100 name_map: None,
101 hreflang: None,
102 height: None,
103 width: None,
104 preview: None,
105 extra: BTreeMap::new(),
106 }
107 }
108
109 #[must_use]
111 pub fn mention(href: Url) -> Self {
112 let mut link = Self::new(href);
113 link.kind = OneOrMany::one(kind::link::MENTION.to_owned());
114 link
115 }
116
117 #[must_use]
119 pub fn hashtag(href: Url, name: impl Into<String>) -> Self {
120 let mut link = Self::new(href);
121 link.kind = OneOrMany::one(kind::link::HASHTAG.to_owned());
122 link.name = Some(name.into());
123 link
124 }
125
126 #[must_use]
128 pub fn is_kind(&self, kind: &str) -> bool {
129 self.kind.iter().any(|k| k == kind)
130 }
131}
132
133impl HasId for Link {
134 fn id(&self) -> Option<&Url> {
135 self.id.as_ref()
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use pretty_assertions::assert_eq;
142 use serde_json::json;
143
144 use super::*;
145
146 #[test]
147 fn link_defaults_type_to_link() {
148 let link = Link::new(Url::parse("https://example/x").unwrap());
149 let v = serde_json::to_value(&link).unwrap();
150 assert_eq!(v["type"], json!("Link"));
151 }
152
153 #[test]
154 fn mention_sets_mention_type() {
155 let link = Link::mention(Url::parse("https://example/u/alice").unwrap());
156 assert!(link.is_kind(kind::link::MENTION));
157 }
158
159 #[test]
160 fn mastodon_style_mention_roundtrips() {
161 let raw = json!({
162 "type": "Mention",
163 "href": "https://mastodon.social/@alice",
164 "name": "@alice@mastodon.social"
165 });
166 let link: Link = serde_json::from_value(raw.clone()).unwrap();
167 assert_eq!(link.name.as_deref(), Some("@alice@mastodon.social"));
168 let back = serde_json::to_value(&link).unwrap();
169 assert_eq!(back, raw);
170 }
171}