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}