webfinger_rs/types/
request.rs

1use http::Uri;
2use serde::{Deserialize, Serialize};
3use serde_with::{DisplayFromStr, serde_as};
4
5use crate::{Error, Rel};
6
7/// A WebFinger request.
8///
9/// This represents the request portion of a WebFinger query that can be executed against a
10/// WebFinger server.
11///
12/// See [RFC 7033 Section 4](https://www.rfc-editor.org/rfc/rfc7033.html#section-4) for more
13/// information.
14///
15/// # Examples
16///
17/// ```rust
18/// use webfinger_rs::WebFingerRequest;
19///
20/// let request = WebFingerRequest::builder("acct:carol@example.com")?
21///     .host("example.com")
22///     .rel("http://webfinger.net/rel/profile-page")
23///     .build();
24/// # Ok::<(), Box<dyn std::error::Error>>(())
25/// ```
26///
27/// To execute the query, enable the `reqwest` feature and call `query.execute()`.
28///
29/// ```rust
30/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
31/// # use webfinger_rs::WebFingerRequest;
32/// # let request = WebFingerRequest::builder("acct:carol@example.com")?.build();
33/// let response = request.execute_reqwest().await?;
34/// # Ok(())
35/// # }
36/// ```
37///
38/// `Request` can be used as an Axum extractor as it implements [`axum::extract::FromRequestParts`].
39///
40/// ```rust
41/// use webfinger_rs::{WebFingerRequest, WebFingerResponse};
42///
43/// async fn handler(request: WebFingerRequest) -> WebFingerResponse {
44///     // ... handle the request ...
45/// # WebFingerResponse::new("")
46/// }
47/// ```
48#[serde_as]
49#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
50pub struct Request {
51    /// Query target.
52    ///
53    /// This is the URI of the resource to query. It will be stored in the `resource` query
54    /// parameter.
55    ///
56    /// TODO: This could be a newtype that represents the resource and makes it easier to extract
57    /// the values / parse into the right types (e.g. `acct:` URIs).
58    #[serde_as(as = "DisplayFromStr")]
59    pub resource: Uri,
60
61    /// The host to query
62    ///
63    /// TODO: this might be better as an `Option<Uri>` or `Option<Host>` or something similar. When
64    /// the resource has a host part, it should be used unless this field is set.
65    pub host: String,
66
67    /// Link relation types
68    ///
69    /// This is a list of link relation types to query for. Each link relation type will be stored
70    /// in a `rel` query parameter.
71    pub rels: Vec<Rel>,
72}
73
74impl Request {
75    /// Creates a new WebFinger request.
76    pub fn new(resource: Uri) -> Self {
77        Self {
78            host: String::new(),
79            resource,
80            rels: Vec::new(),
81        }
82    }
83
84    /// Creates a new [`Builder`] for a WebFinger request.
85    pub fn builder<U>(uri: U) -> Result<Builder, Error>
86    where
87        Uri: TryFrom<U>,
88        <Uri as TryFrom<U>>::Error: Into<Error>,
89    {
90        Builder::new(uri)
91    }
92}
93
94/// A builder for a WebFinger request.
95///
96/// This is used to construct a [`Request`] for a WebFinger query.
97///
98/// # Examples
99///
100/// ```rust
101/// use webfinger_rs::WebFingerRequest;
102///
103/// let query = WebFingerRequest::builder("acct:carol@example.com")?
104///     .host("example.com")
105///     .rel("http://webfinger.net/rel/profile-page")
106///     .build();
107/// # Ok::<(), Box<dyn std::error::Error>>(())
108/// ```
109#[derive(Debug)]
110pub struct Builder {
111    request: Request,
112}
113
114impl Builder {
115    /// Creates a new WebFinger request builder.
116    ///
117    /// This will use the given URI as the resource for the query.
118    ///
119    /// # Errors
120    ///
121    /// This will return an error if the URI is invalid.
122    pub fn new<U>(uri: U) -> Result<Self, Error>
123    where
124        Uri: TryFrom<U>,
125        <Uri as TryFrom<U>>::Error: Into<Error>,
126    {
127        TryFrom::try_from(uri)
128            .map(|uri| Self {
129                request: Request::new(uri),
130            })
131            .map_err(Into::into)
132    }
133
134    /// Sets the host for the query.
135    pub fn host<S: Into<String>>(mut self, host: S) -> Self {
136        self.request.host = host.into();
137        self
138    }
139
140    /// Adds a link relation type to the query.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use webfinger_rs::WebFingerRequest;
146    ///
147    /// let query = WebFingerRequest::builder("acct:carol@example.com")?
148    ///     .rel("http://webfinger.net/rel/profile-page")
149    ///     .build();
150    /// # Ok::<(), Box<dyn std::error::Error>>(())
151    /// ```
152    pub fn rel<R: Into<Rel>>(mut self, rel: R) -> Self {
153        self.request.rels.push(rel.into());
154        self
155    }
156
157    /// Builds the WebFinger request.
158    pub fn build(self) -> Request {
159        self.request
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use http::Uri;
166
167    use super::*;
168
169    /// https://www.rfc-editor.org/rfc/rfc7033.html#section-3.1
170    #[test]
171    fn example_3_1() {
172        let resource = "acct:carol@example.com".parse().unwrap();
173        let rel = Rel::from("http://openid.net/specs/connect/1.0/issuer");
174        let host = "example.com".parse().unwrap();
175        let query = Request {
176            host,
177            resource,
178            rels: vec![rel],
179        };
180        let uri = Uri::try_from(&query).unwrap();
181
182        // The RFC unnecessarily percent-encodes this to:
183        // `"/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fopenid.net%
184        // 2Fspecs%2Fconnect%2F1.0%2Fissuer"`
185        assert_eq!(
186            uri.to_string(),
187            "https://example.com/.well-known/webfinger?resource=acct:carol@example.com&rel=http://openid.net/specs/connect/1.0/issuer",
188        );
189    }
190
191    /// https://www.rfc-editor.org/rfc/rfc7033.html#section-3.2
192    #[test]
193    fn example_3_2() {
194        let resource = "http://blog.example.com/article/id/314".parse().unwrap();
195        let query = Request {
196            host: "blog.example.com".parse().unwrap(),
197            resource,
198            rels: vec![],
199        };
200        let uri = Uri::try_from(&query).unwrap();
201
202        // The RFC unnecessarily percent-encodes this to:
203        // /.well-known/webfinger?resource=http%3A%2F%2Fblog.example.com%2Farticle%2Fid%2F314
204        assert_eq!(
205            uri.to_string(),
206            "https://blog.example.com/.well-known/webfinger?resource=http://blog.example.com/article/id/314",
207        );
208    }
209}