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}