feedparser_rs/types/thread.rs
1use super::common::{MimeType, SmallString, Url};
2
3/// Atom Threading Extensions (RFC 4685) in-reply-to element
4///
5/// Represents a reference to another entry that this entry is a reply to.
6/// Namespace URI: `http://purl.org/syndication/thread/1.0`
7///
8/// # Examples
9///
10/// ```
11/// use feedparser_rs::InReplyTo;
12///
13/// let reply = InReplyTo {
14/// ref_: Some("tag:example.com,2024:post/1".into()),
15/// href: Some("https://example.com/post/1".into()),
16/// type_: Some("text/html".into()),
17/// source: Some("https://example.com/feed.xml".into()),
18/// };
19///
20/// assert_eq!(reply.ref_.as_deref(), Some("tag:example.com,2024:post/1"));
21/// ```
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct InReplyTo {
24 /// IRI of the entry being replied to (ref attribute)
25 ///
26 /// Required by RFC 4685, but we tolerate missing values (bozo pattern).
27 /// Empty-string values from malformed feeds are normalized to None.
28 /// Field named `ref_` to avoid Rust keyword clash.
29 pub ref_: Option<SmallString>,
30
31 /// URL where the referenced entry can be found (href attribute)
32 ///
33 /// # Security
34 ///
35 /// This URL comes from untrusted feed input and has NOT been validated.
36 /// Applications MUST validate URLs before fetching to prevent SSRF.
37 pub href: Option<Url>,
38
39 /// MIME type of the linked resource (type attribute)
40 ///
41 /// Field named `type_` to avoid Rust keyword clash.
42 pub type_: Option<MimeType>,
43
44 /// IRI of the feed containing the referenced entry (source attribute)
45 ///
46 /// Not to be confused with [`Entry::source`](super::entry::Entry::source)
47 /// which is the RSS/Atom source feed reference. This field is the
48 /// RFC 4685 `source` attribute: an IRI identifying the feed that
49 /// contains the entry being replied to.
50 ///
51 /// # Security
52 ///
53 /// This URL comes from untrusted feed input and has NOT been validated.
54 /// Applications MUST validate URLs before fetching to prevent SSRF.
55 pub source: Option<Url>,
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn test_in_reply_to_default() {
64 let reply = InReplyTo::default();
65 assert!(reply.ref_.is_none());
66 assert!(reply.href.is_none());
67 assert!(reply.type_.is_none());
68 assert!(reply.source.is_none());
69 }
70
71 #[test]
72 fn test_in_reply_to_partial_construction() {
73 let reply = InReplyTo {
74 ref_: Some("tag:example.com,2024:post/1".into()),
75 ..Default::default()
76 };
77 assert_eq!(reply.ref_.as_deref(), Some("tag:example.com,2024:post/1"));
78 assert!(reply.href.is_none());
79 assert!(reply.type_.is_none());
80 assert!(reply.source.is_none());
81 }
82
83 #[test]
84 fn test_in_reply_to_equality() {
85 let a = InReplyTo {
86 ref_: Some("tag:example.com,2024:1".into()),
87 href: Some("https://example.com/1".into()),
88 type_: Some("text/html".into()),
89 source: Some("https://example.com/feed.xml".into()),
90 };
91 let b = a.clone();
92 assert_eq!(a, b);
93 }
94
95 #[test]
96 #[allow(clippy::redundant_clone)]
97 fn test_in_reply_to_clone() {
98 let original = InReplyTo {
99 ref_: Some("tag:example.com,2024:post/1".into()),
100 href: Some("https://example.com/post/1".into()),
101 type_: Some("text/html".into()),
102 source: Some("https://example.com/feed.xml".into()),
103 };
104 let cloned = original.clone();
105 assert_eq!(cloned.ref_.as_deref(), Some("tag:example.com,2024:post/1"));
106 assert_eq!(cloned.href.as_deref(), Some("https://example.com/post/1"));
107 assert_eq!(cloned.type_.as_deref(), Some("text/html"));
108 assert_eq!(
109 cloned.source.as_deref(),
110 Some("https://example.com/feed.xml")
111 );
112 }
113}