Skip to main content

webfinger_rs/types/
request.rs

1use serde::{Deserialize, Serialize};
2use serde_with::{DisplayFromStr, serde_as};
3
4use crate::{Error, Rel, Resource};
5
6/// A WebFinger request.
7///
8/// This represents the request portion of a WebFinger query that can be executed against a
9/// WebFinger server.
10///
11/// `Request` stores three pieces of information that map directly to the outgoing request URL:
12///
13/// - `resource` becomes the `resource=` query parameter.
14/// - `host` becomes the HTTPS authority for the request URL.
15/// - Each value in `rels` becomes another `rel=` query parameter, in insertion order.
16///
17/// In other words, this request:
18///
19/// ```rust
20/// use webfinger_rs::WebFingerRequest;
21///
22/// let request = WebFingerRequest::builder("acct:carol@example.com")?
23///     .host("example.com")
24///     .rel("http://webfinger.net/rel/profile-page")
25///     .rel("http://webfinger.net/rel/avatar")
26///     .build();
27/// # let _ = request;
28/// # Ok::<(), Box<dyn std::error::Error>>(())
29/// ```
30///
31/// maps to this URL shape:
32///
33/// ```text
34/// https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Favatar
35/// ```
36///
37/// `host` is required when you want to turn the request into an outgoing HTTP request, because the
38/// WebFinger endpoint is always built as `https://{host}/.well-known/webfinger?...`. For `acct:`
39/// resources, set `host` to the domain that serves WebFinger for that account. In the common case,
40/// that is the same domain that appears after `@` in the `acct:` URI.
41///
42/// `resource` must be an absolute URI, not a relative reference. `acct:` resources should include
43/// the full account URI, such as `acct:carol@example.com`, not just `carol@example.com` or
44/// `@carol@example.com`. Hierarchical URIs such as `https://example.com/users/carol` are also
45/// accepted.
46///
47/// Repeated relation filters are encoded as repeated `rel` query parameters rather than as a
48/// comma-separated list.
49///
50/// See: [RFC 7033 section 4.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1) for
51/// query-construction rules and parameter encoding.
52///
53/// See: [RFC 7565 section 3](https://www.rfc-editor.org/rfc/rfc7565.html#section-3) for the
54/// `acct:` URI syntax used by account resources.
55///
56/// # Examples
57///
58/// ```rust
59/// use webfinger_rs::WebFingerRequest;
60///
61/// let request = WebFingerRequest::builder("acct:carol@example.com")?
62///     .host("example.com")
63///     .rel("http://webfinger.net/rel/profile-page")
64///     .build();
65/// # Ok::<(), Box<dyn std::error::Error>>(())
66/// ```
67///
68/// To execute the query, enable the `reqwest` feature and call [`Request::execute_reqwest`].
69///
70/// ```rust
71/// # #[cfg(feature = "reqwest")]
72/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
73/// # use webfinger_rs::WebFingerRequest;
74/// # let request = WebFingerRequest::builder("acct:carol@example.com")?
75/// #     .host("example.com")
76/// #     .build();
77/// let response = request.execute_reqwest().await?;
78/// # Ok(())
79/// # }
80/// ```
81///
82/// `Request` can be used as an Axum extractor as it implements [`axum::extract::FromRequestParts`].
83///
84/// ```rust
85/// use webfinger_rs::{WebFingerRequest, WebFingerResponse};
86///
87/// async fn handler(request: WebFingerRequest) -> WebFingerResponse {
88///     // ... handle the request ...
89/// # WebFingerResponse::new("acct:carol@example.com")
90/// }
91/// ```
92#[serde_as]
93#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub struct Request {
95    /// Query target.
96    ///
97    /// This is the absolute URI of the resource to query. It will be stored in the `resource`
98    /// query parameter.
99    ///
100    /// For account lookups, use the full `acct:` URI, for example `acct:carol@example.com`.
101    /// Relative references such as `carol`, `/relative`, `../x`, and empty values are rejected by
102    /// builders and first-party server extractors.
103    ///
104    /// See: [RFC 7565 section 3](https://www.rfc-editor.org/rfc/rfc7565.html#section-3).
105    ///
106    #[serde_as(as = "DisplayFromStr")]
107    pub resource: Resource,
108
109    /// The host to query.
110    ///
111    /// This becomes the HTTPS authority of the final request URL. When converting this request to
112    /// an outgoing [`http::Uri`], the crate builds
113    /// `https://{host}/.well-known/webfinger?...`.
114    ///
115    /// Set this explicitly before executing the request or converting it into an outgoing URL.
116    /// For `acct:` resources, this is usually the domain part of the account identifier.
117    ///
118    /// See: [RFC 7033 section 4.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1).
119    ///
120    /// TODO: this might be better as an `Option<Uri>` or `Option<Host>` or something similar. When
121    /// the resource has a host part, it should be used unless this field is set.
122    pub host: String,
123
124    /// Link relation types
125    ///
126    /// This is a list of link relation types to query for. Each link relation type will be stored
127    /// in its own `rel` query parameter.
128    ///
129    /// See: [RFC 7033 section 4.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1).
130    pub rels: Vec<Rel>,
131}
132
133impl Request {
134    /// Creates a new WebFinger request.
135    ///
136    /// This low-level constructor accepts an already validated [`Resource`]. Use
137    /// [`Request::builder`] or `str::parse::<Resource>()` when accepting untrusted resource text.
138    pub fn new(resource: Resource) -> Self {
139        Self {
140            host: String::new(),
141            resource,
142            rels: Vec::new(),
143        }
144    }
145
146    /// Creates a new [`Builder`] for a WebFinger request.
147    pub fn builder<U>(uri: U) -> Result<Builder, Error>
148    where
149        Resource: TryFrom<U>,
150        <Resource as TryFrom<U>>::Error: Into<Error>,
151    {
152        Builder::new(uri)
153    }
154}
155
156/// A builder for a WebFinger request.
157///
158/// This is used to construct a [`Request`] for a WebFinger query.
159///
160/// # Examples
161///
162/// ```rust
163/// use webfinger_rs::WebFingerRequest;
164///
165/// let query = WebFingerRequest::builder("acct:carol@example.com")?
166///     .host("example.com")
167///     .rel("http://webfinger.net/rel/profile-page")
168///     .build();
169/// # Ok::<(), Box<dyn std::error::Error>>(())
170/// ```
171#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
172pub struct Builder {
173    request: Request,
174}
175
176impl Builder {
177    /// Creates a new WebFinger request builder.
178    ///
179    /// This will use the given URI as the resource for the query.
180    ///
181    /// The resource must be an absolute URI with a scheme. For account lookups, pass the complete
182    /// `acct:` URI, such as `acct:carol@example.com`.
183    ///
184    /// See: [RFC 7565 section 3](https://www.rfc-editor.org/rfc/rfc7565.html#section-3).
185    ///
186    /// # Errors
187    ///
188    /// This will return an error if the URI is invalid or if it is a relative reference rather than
189    /// an absolute URI.
190    pub fn new<U>(uri: U) -> Result<Self, Error>
191    where
192        Resource: TryFrom<U>,
193        <Resource as TryFrom<U>>::Error: Into<Error>,
194    {
195        Resource::try_from(uri)
196            .map(|resource| Self {
197                request: Request::new(resource),
198            })
199            .map_err(Into::into)
200    }
201
202    /// Sets the host for the query.
203    ///
204    /// This host is used as the authority in the final HTTPS request URL.
205    ///
206    /// See: [RFC 7033 section 4.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1).
207    pub fn host<S: Into<String>>(mut self, host: S) -> Self {
208        self.request.host = host.into();
209        self
210    }
211
212    /// Adds a link relation type to the query.
213    ///
214    /// Each call appends another `rel` query parameter to the outgoing request URL.
215    ///
216    /// See: [RFC 7033 section 4.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1).
217    ///
218    /// # Examples
219    ///
220    /// ```rust
221    /// use webfinger_rs::WebFingerRequest;
222    ///
223    /// let query = WebFingerRequest::builder("acct:carol@example.com")?
224    ///     .rel("http://webfinger.net/rel/profile-page")
225    ///     .build();
226    /// # Ok::<(), Box<dyn std::error::Error>>(())
227    /// ```
228    pub fn rel<R: AsRef<str>>(mut self, rel: R) -> Self {
229        self.request.rels.push(Rel::new(rel));
230        self
231    }
232
233    /// Builds the WebFinger request.
234    ///
235    /// # Examples
236    ///
237    /// Build a request for an `acct:` resource and inspect the final URL:
238    ///
239    /// ```rust
240    /// use http::Uri;
241    /// use webfinger_rs::WebFingerRequest;
242    ///
243    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
244    ///     .host("example.com")
245    ///     .rel("http://webfinger.net/rel/profile-page")
246    ///     .build();
247    ///
248    /// let uri = Uri::try_from(&request)?;
249    ///
250    /// assert_eq!(
251    ///     uri.to_string(),
252    ///     "https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page",
253    /// );
254    /// # Ok::<(), Box<dyn std::error::Error>>(())
255    /// ```
256    ///
257    /// Multiple relation filters become repeated `rel` query parameters:
258    ///
259    /// ```rust
260    /// use http::Uri;
261    /// use webfinger_rs::WebFingerRequest;
262    ///
263    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
264    ///     .host("example.com")
265    ///     .rel("http://webfinger.net/rel/profile-page")
266    ///     .rel("http://webfinger.net/rel/avatar")
267    ///     .build();
268    ///
269    /// let uri = Uri::try_from(&request)?;
270    ///
271    /// assert_eq!(
272    ///     uri.to_string(),
273    ///     "https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Favatar",
274    /// );
275    /// # Ok::<(), Box<dyn std::error::Error>>(())
276    /// ```
277    pub fn build(self) -> Request {
278        self.request
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use http::Uri;
285
286    use super::*;
287
288    /// https://www.rfc-editor.org/rfc/rfc7033.html#section-3.1
289    #[test]
290    fn example_3_1() {
291        let resource = "acct:carol@example.com".parse().unwrap();
292        let rel = Rel::new("http://openid.net/specs/connect/1.0/issuer");
293        let host = "example.com".parse().unwrap();
294        let query = Request {
295            host,
296            resource,
297            rels: vec![rel],
298        };
299        let uri = Uri::try_from(&query).unwrap();
300
301        assert_eq!(
302            uri.to_string(),
303            "https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer",
304        );
305    }
306
307    /// https://www.rfc-editor.org/rfc/rfc7033.html#section-3.2
308    #[test]
309    fn example_3_2() {
310        let resource = "http://blog.example.com/article/id/314".parse().unwrap();
311        let query = Request {
312            host: "blog.example.com".parse().unwrap(),
313            resource,
314            rels: vec![],
315        };
316        let uri = Uri::try_from(&query).unwrap();
317
318        assert_eq!(
319            uri.to_string(),
320            "https://blog.example.com/.well-known/webfinger?resource=http%3A%2F%2Fblog.example.com%2Farticle%2Fid%2F314",
321        );
322    }
323}