Skip to main content

discovery/
event.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::classifier::Validity;
5
6pub const EVENT_VERSION: u8 = 1;
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "kebab-case")]
10pub enum Via {
11    LinkHeader,
12    HtmlLink,
13    WellKnown,
14    PageApi,
15    Response,
16    Catalog,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct DiscoveryEvent {
21    pub v: u8,
22    pub standard: String,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub aliases: Option<Vec<String>>,
25    pub origin: String,
26    pub url: String,
27    pub via: Via,
28    pub page: String,
29    #[serde(rename = "targetId")]
30    pub target_id: String,
31    pub status: Validity,
32    pub summary: Value,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub reason: Option<String>,
35}
36
37impl DiscoveryEvent {
38    #[must_use]
39    pub fn to_json(&self) -> String {
40        serde_json::to_string(self).unwrap_or_default()
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn serialize_roundtrip() {
50        let event = DiscoveryEvent {
51            v: EVENT_VERSION,
52            standard: "ard".into(),
53            aliases: None,
54            origin: "https://example.com".into(),
55            url: "https://example.com/.well-known/ard.json".into(),
56            via: Via::WellKnown,
57            page: "https://example.com/shop".into(),
58            target_id: "target1".into(),
59            status: Validity::Valid,
60            summary: serde_json::json!({"name": "test"}),
61            reason: None,
62        };
63        let json = event.to_json();
64        let parsed: DiscoveryEvent = serde_json::from_str(&json).unwrap();
65        assert_eq!(parsed, event);
66    }
67
68    #[test]
69    fn via_serializes_as_kebab() {
70        assert_eq!(
71            serde_json::to_string(&Via::LinkHeader).unwrap(),
72            r#""link-header""#
73        );
74        assert_eq!(
75            serde_json::to_string(&Via::HtmlLink).unwrap(),
76            r#""html-link""#
77        );
78        assert_eq!(
79            serde_json::to_string(&Via::WellKnown).unwrap(),
80            r#""well-known""#
81        );
82        assert_eq!(
83            serde_json::to_string(&Via::PageApi).unwrap(),
84            r#""page-api""#
85        );
86        assert_eq!(
87            serde_json::to_string(&Via::Response).unwrap(),
88            r#""response""#
89        );
90        assert_eq!(
91            serde_json::to_string(&Via::Catalog).unwrap(),
92            r#""catalog""#
93        );
94    }
95
96    #[test]
97    fn reason_omitted_when_none() {
98        let event = DiscoveryEvent {
99            v: EVENT_VERSION,
100            standard: "test".into(),
101            aliases: None,
102            origin: "https://x.com".into(),
103            url: "https://x.com/f".into(),
104            via: Via::Response,
105            page: "https://x.com/".into(),
106            target_id: "t".into(),
107            status: Validity::Valid,
108            summary: serde_json::json!({}),
109            reason: None,
110        };
111        let json = event.to_json();
112        assert!(!json.contains("reason"));
113    }
114
115    #[test]
116    fn reason_included_when_present() {
117        let event = DiscoveryEvent {
118            v: EVENT_VERSION,
119            standard: "test".into(),
120            aliases: None,
121            origin: "https://x.com".into(),
122            url: "https://x.com/f".into(),
123            via: Via::Response,
124            page: "https://x.com/".into(),
125            target_id: "t".into(),
126            status: Validity::Invalid,
127            summary: serde_json::json!(null),
128            reason: Some("bad json".into()),
129        };
130        let json = event.to_json();
131        assert!(json.contains("\"reason\":\"bad json\""));
132    }
133
134    #[test]
135    fn event_version_is_one() {
136        assert_eq!(EVENT_VERSION, 1);
137    }
138
139    #[test]
140    fn aliases_omitted_when_none() {
141        let event = DiscoveryEvent {
142            v: EVENT_VERSION,
143            standard: "ard".into(),
144            aliases: None,
145            origin: "https://x.com".into(),
146            url: "https://x.com/f".into(),
147            via: Via::WellKnown,
148            page: "https://x.com/".into(),
149            target_id: "t".into(),
150            status: Validity::Valid,
151            summary: serde_json::json!({}),
152            reason: None,
153        };
154        let json = event.to_json();
155        assert!(!json.contains("aliases"));
156    }
157
158    #[test]
159    fn aliases_included_when_present() {
160        let event = DiscoveryEvent {
161            v: EVENT_VERSION,
162            standard: "ard".into(),
163            aliases: Some(vec!["ai-catalog".into()]),
164            origin: "https://x.com".into(),
165            url: "https://x.com/.well-known/ard.json".into(),
166            via: Via::WellKnown,
167            page: "https://x.com/".into(),
168            target_id: "t".into(),
169            status: Validity::Valid,
170            summary: serde_json::json!({"name": "test"}),
171            reason: None,
172        };
173        let json = event.to_json();
174        assert!(json.contains("\"aliases\":[\"ai-catalog\"]"));
175        let parsed: DiscoveryEvent = serde_json::from_str(&json).unwrap();
176        assert_eq!(parsed.aliases, Some(vec!["ai-catalog".to_string()]));
177    }
178
179    #[test]
180    fn aliases_roundtrip() {
181        let event = DiscoveryEvent {
182            v: EVENT_VERSION,
183            standard: "ard".into(),
184            aliases: Some(vec!["ai-catalog".into()]),
185            origin: "https://example.com".into(),
186            url: "https://example.com/.well-known/ard.json".into(),
187            via: Via::WellKnown,
188            page: "https://example.com/".into(),
189            target_id: "t".into(),
190            status: Validity::Valid,
191            summary: serde_json::json!({}),
192            reason: None,
193        };
194        let json = event.to_json();
195        let parsed: DiscoveryEvent = serde_json::from_str(&json).unwrap();
196        assert_eq!(parsed, event);
197    }
198}