twilight_http/request/
base.rs

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