twilight_http/request/
base.rs

1use super::{Form, Method};
2use crate::{
3    error::Error,
4    routing::{Path, Route},
5};
6use http::header::{HeaderMap, HeaderName, HeaderValue};
7use serde::Serialize;
8
9/// Builder to create a customized request.
10///
11/// # Examples
12///
13/// Create a request to create a message with a content of "test" in a
14/// channel with an ID of 1:
15///
16/// ```
17/// use twilight_http::{request::Request, routing::Route};
18///
19/// let body = br#"{
20///     "content": "test"
21/// }"#
22/// .to_vec();
23///
24/// let request = Request::builder(&Route::CreateMessage { channel_id: 1 })
25///     .body(body)
26///     .build();
27/// ```
28#[derive(Debug)]
29#[must_use = "request has not been fully built"]
30pub struct RequestBuilder(Result<Request, Error>);
31
32impl RequestBuilder {
33    /// Create a new request builder.
34    pub fn new(route: &Route<'_>) -> Self {
35        Self(Ok(Request::from_route(route)))
36    }
37
38    /// Create a request with raw information about the method, ratelimiting
39    /// path, and URL path and query.
40    ///
41    /// The path and query should not include the leading slash as that is
42    /// prefixed by the client. In the URL
43    /// `https://discord.com/api/vX/channels/123/pins` the "path and query"
44    /// is considered to be `channels/123/pins`.
45    ///
46    /// # Examples
47    ///
48    /// Create a request from a method and the URL path and query
49    /// `channels/123/pins`:
50    ///
51    /// ```
52    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
53    /// use std::str::FromStr;
54    /// use twilight_http::{
55    ///     request::{Method, RequestBuilder},
56    ///     routing::Path,
57    /// };
58    ///
59    /// let method = Method::Post;
60    /// let path_and_query = "channels/123/pins".to_owned();
61    /// let ratelimit_path = Path::from_str(&path_and_query)?;
62    ///
63    /// let _request = RequestBuilder::raw(method, ratelimit_path, path_and_query).build();
64    /// # Ok(()) }
65    /// ```
66    pub const fn raw(method: Method, ratelimit_path: Path, path_and_query: String) -> Self {
67        Self(Ok(Request {
68            body: None,
69            form: None,
70            headers: None,
71            method,
72            path: path_and_query,
73            ratelimit_path,
74            use_authorization_token: true,
75        }))
76    }
77
78    /// Consume the builder, returning the built request.
79    ///
80    /// # Errors
81    ///
82    /// Returns an [`ErrorType::Json`] error type JSON input could not be
83    /// serialized.
84    ///
85    /// [`ErrorType::Json`]: crate::error::ErrorType::Json
86    #[allow(clippy::missing_const_for_fn)]
87    #[must_use = "request information is not useful on its own and must be acted on"]
88    pub fn build(self) -> Result<Request, Error> {
89        self.0
90    }
91
92    /// Set the contents of the body.
93    pub fn body(mut self, body: Vec<u8>) -> Self {
94        if let Ok(request) = self.0.as_mut() {
95            request.body = Some(body);
96        }
97
98        self
99    }
100
101    /// Set the multipart form.
102    #[allow(clippy::missing_const_for_fn)]
103    pub fn form(mut self, form: Form) -> Self {
104        if let Ok(request) = self.0.as_mut() {
105            request.form = Some(form);
106        }
107
108        self
109    }
110
111    /// Set the headers to add.
112    pub fn headers(mut self, iter: impl Iterator<Item = (HeaderName, HeaderValue)>) -> Self {
113        if let Ok(request) = self.0.as_mut() {
114            request.headers.replace(iter.collect());
115        }
116
117        self
118    }
119
120    /// Set the body, to be serialized as JSON.
121    pub fn json(mut self, to: &impl Serialize) -> Self {
122        self.0 = self.0.and_then(|mut request| {
123            let bytes = crate::json::to_vec(to).map_err(Error::json)?;
124            request.body = Some(bytes);
125
126            Ok(request)
127        });
128
129        self
130    }
131
132    /// Whether to use the client's authorization token in the request, if one
133    /// is set.
134    ///
135    /// This is primarily useful for executing webhooks.
136    pub fn use_authorization_token(mut self, use_authorization_token: bool) -> Self {
137        if let Ok(request) = self.0.as_mut() {
138            request.use_authorization_token = use_authorization_token;
139        }
140
141        self
142    }
143}
144
145#[derive(Clone, Debug)]
146pub struct Request {
147    pub(crate) body: Option<Vec<u8>>,
148    pub(crate) form: Option<Form>,
149    pub(crate) headers: Option<HeaderMap<HeaderValue>>,
150    pub(crate) method: Method,
151    pub(crate) path: String,
152    pub(crate) ratelimit_path: Path,
153    pub(crate) use_authorization_token: bool,
154}
155
156impl Request {
157    /// Create a new request builder.
158    ///
159    /// # Examples
160    ///
161    /// Create a request to create a message with a content of "test" in a
162    /// channel with an ID of 1:
163    ///
164    /// ```
165    /// use twilight_http::{request::Request, routing::Route};
166    ///
167    /// let body = br#"{
168    ///     "content": "test"
169    /// }"#
170    /// .to_vec();
171    ///
172    /// let request = Request::builder(&Route::CreateMessage { channel_id: 1 })
173    ///     .body(body)
174    ///     .build();
175    /// ```
176    pub fn builder(route: &Route<'_>) -> RequestBuilder {
177        RequestBuilder::new(route)
178    }
179
180    /// Create a request from only its route information.
181    ///
182    /// If you need to set additional configurations like the body then use
183    /// [`builder`].
184    ///
185    /// # Examples
186    ///
187    /// Create a request to get a message with an ID of 2 in a channel with an
188    /// ID of 1:
189    ///
190    /// ```
191    /// use twilight_http::{request::Request, routing::Route};
192    ///
193    /// let request = Request::from_route(&Route::GetMessage {
194    ///     channel_id: 1,
195    ///     message_id: 2,
196    /// });
197    /// ```
198    ///
199    /// [`builder`]: Self::builder
200    pub fn from_route(route: &Route<'_>) -> Self {
201        Self {
202            body: None,
203            form: None,
204            headers: None,
205            method: route.method(),
206            path: route.to_string(),
207            ratelimit_path: route.to_path(),
208            use_authorization_token: true,
209        }
210    }
211
212    /// Body of the request, if any.
213    pub fn body(&self) -> Option<&[u8]> {
214        self.body.as_deref()
215    }
216
217    /// Multipart form of the request, if any.
218    pub const fn form(&self) -> Option<&Form> {
219        self.form.as_ref()
220    }
221
222    /// Headers to set in the request, if any.
223    pub const fn headers(&self) -> Option<&HeaderMap<HeaderValue>> {
224        self.headers.as_ref()
225    }
226
227    /// Method when sending the request.
228    pub const fn method(&self) -> Method {
229        self.method
230    }
231
232    /// String path of the full URL.
233    pub fn path(&self) -> &str {
234        &self.path
235    }
236
237    /// Path used for ratelimiting.
238    pub const fn ratelimit_path(&self) -> &Path {
239        &self.ratelimit_path
240    }
241
242    /// Whether to use the client's authorization token in the request.
243    pub const fn use_authorization_token(&self) -> bool {
244        self.use_authorization_token
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::RequestBuilder;
251    use static_assertions::assert_impl_all;
252    use std::fmt::Debug;
253
254    assert_impl_all!(RequestBuilder: Debug, Send, Sync);
255}