Skip to main content

actpub_nodeinfo/
discovery.rs

1//! The `NodeInfo` discovery document served at `/.well-known/nodeinfo`.
2
3use serde::{Deserialize, Serialize};
4use url::Url;
5
6use crate::schema::Version;
7
8/// Common prefix shared by all `NodeInfo` schema `rel` URIs.
9///
10/// Specific versions are formed by appending `"2.0"`, `"2.1"`, etc., giving
11/// URIs such as `http://nodeinfo.diaspora.software/ns/schema/2.1`.
12pub const SCHEMA_REL_PREFIX: &str = "http://nodeinfo.diaspora.software/ns/schema/";
13
14/// A `NodeInfo` discovery document, served at `/.well-known/nodeinfo`.
15///
16/// Points clients at one or more versioned `NodeInfo` schema documents. At
17/// least one link should be present; clients typically pick the most
18/// recent version they support.
19#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[non_exhaustive]
21pub struct Discovery {
22    /// Links to specific schema documents.
23    pub links: Vec<DiscoveryLink>,
24}
25
26impl Discovery {
27    /// Creates a discovery document advertising a single `NodeInfo` endpoint.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// # use actpub_nodeinfo::{Discovery, Version};
33    /// let disco = Discovery::for_version(
34    ///     Version::V2_1,
35    ///     "https://example.com/nodeinfo/2.1".parse().unwrap(),
36    /// );
37    /// assert_eq!(disco.links.len(), 1);
38    /// ```
39    #[must_use]
40    pub fn for_version(version: Version, href: Url) -> Self {
41        Self {
42            links: vec![DiscoveryLink::new(version, href)],
43        }
44    }
45
46    /// Appends another version endpoint and returns `self` for chaining.
47    #[must_use]
48    pub fn with_version(mut self, version: Version, href: Url) -> Self {
49        self.links.push(DiscoveryLink::new(version, href));
50        self
51    }
52
53    /// Returns the link with the highest advertised `NodeInfo` version this
54    /// crate understands.
55    ///
56    /// Prefers 2.1 over 2.0; returns `None` if no recognised schema rel is
57    /// present.
58    #[must_use]
59    pub fn preferred_link(&self) -> Option<&DiscoveryLink> {
60        self.find_link(Version::V2_1)
61            .or_else(|| self.find_link(Version::V2_0))
62    }
63
64    /// Returns the link matching the given version, if any.
65    #[must_use]
66    pub fn find_link(&self, version: Version) -> Option<&DiscoveryLink> {
67        self.links.iter().find(|l| l.rel == version.schema_uri())
68    }
69}
70
71/// A single discovery link pointing at a specific schema version.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct DiscoveryLink {
75    /// Schema relation URI (e.g. `http://nodeinfo.diaspora.software/ns/schema/2.1`).
76    pub rel: String,
77
78    /// URL of the concrete `NodeInfo` document for this version.
79    pub href: Url,
80}
81
82impl DiscoveryLink {
83    /// Constructs a link for a specific `NodeInfo` [`Version`].
84    #[must_use]
85    pub fn new(version: Version, href: Url) -> Self {
86        Self {
87            rel: version.schema_uri().to_owned(),
88            href,
89        }
90    }
91
92    /// Returns the [`Version`] this link advertises, if recognised.
93    #[must_use]
94    pub fn version(&self) -> Option<Version> {
95        match self.rel.as_str() {
96            s if s == Version::V2_1.schema_uri() => Some(Version::V2_1),
97            s if s == Version::V2_0.schema_uri() => Some(Version::V2_0),
98            _ => None,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use pretty_assertions::assert_eq;
106    use serde_json::json;
107
108    use super::*;
109
110    #[test]
111    fn discovery_roundtrips_mastodon_style() {
112        let raw = json!({
113            "links": [
114                {
115                    "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
116                    "href": "https://mastodon.social/nodeinfo/2.0"
117                },
118                {
119                    "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
120                    "href": "https://mastodon.social/nodeinfo/2.1"
121                }
122            ]
123        });
124
125        let d: Discovery = serde_json::from_value(raw.clone()).unwrap();
126        assert_eq!(d.links.len(), 2);
127
128        let preferred = d.preferred_link().unwrap();
129        assert_eq!(preferred.version(), Some(Version::V2_1));
130
131        let back = serde_json::to_value(&d).unwrap();
132        assert_eq!(back, raw);
133    }
134
135    #[test]
136    fn for_version_builds_single_link() {
137        let d = Discovery::for_version(
138            Version::V2_1,
139            "https://example.com/nodeinfo/2.1".parse().unwrap(),
140        );
141        assert_eq!(d.links.len(), 1);
142        assert_eq!(d.links[0].version(), Some(Version::V2_1));
143    }
144
145    #[test]
146    fn discovery_link_version_is_none_for_unknown() {
147        let link = DiscoveryLink::new(Version::V2_0, "https://example.com/ni/99".parse().unwrap());
148        // Tamper with the rel to simulate an unknown schema URI.
149        let mut unknown = link;
150        unknown.rel = "http://example.com/schema/99".to_owned();
151        assert_eq!(unknown.version(), None);
152    }
153}