Skip to main content

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 the multipart form from an existing boundary and buffer.
94    pub fn multipart(mut self, boundary: [u8; 15], buffer: Vec<u8>) -> Self {
95        if let Ok(request) = self.0.as_mut() {
96            request.form = Some(Form::from_parts(boundary, buffer));
97        }
98
99        self
100    }
101
102    /// Set the headers to add.
103    pub fn headers(mut self, iter: impl Iterator<Item = (HeaderName, HeaderValue)>) -> Self {
104        if let Ok(request) = self.0.as_mut() {
105            request.headers.replace(iter.collect());
106        }
107
108        self
109    }
110
111    /// Set the body, to be serialized as JSON.
112    pub fn json(mut self, to: &impl Serialize) -> Self {
113        self.0 = self.0.and_then(|mut request| {
114            let bytes = crate::json::to_vec(to).map_err(Error::json)?;
115            request.body = Some(bytes);
116
117            Ok(request)
118        });
119
120        self
121    }
122
123    /// Set the multipart form.
124    pub fn form(mut self, form: Form) -> Self {
125        if let Ok(request) = self.0.as_mut() {
126            request.form = Some(form);
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 const 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) use_authorization_token: bool,
153}
154
155impl Request {
156    /// Create a new request builder.
157    ///
158    /// # Examples
159    ///
160    /// Create a request to create a message with a content of "test" in a
161    /// channel with an ID of 1:
162    ///
163    /// ```
164    /// use twilight_http::{request::Request, routing::Route};
165    ///
166    /// let body = br#"{
167    ///     "content": "test"
168    /// }"#
169    /// .to_vec();
170    ///
171    /// let request = Request::builder(&Route::CreateMessage { channel_id: 1 })
172    ///     .body(body)
173    ///     .build();
174    /// ```
175    pub fn builder(route: &Route<'_>) -> RequestBuilder {
176        RequestBuilder::new(route)
177    }
178
179    /// Create a request from only its route information.
180    ///
181    /// If you need to set additional configurations like the body then use
182    /// [`builder`].
183    ///
184    /// # Examples
185    ///
186    /// Create a request to get a message with an ID of 2 in a channel with an
187    /// ID of 1:
188    ///
189    /// ```
190    /// use twilight_http::{request::Request, routing::Route};
191    ///
192    /// let request = Request::from_route(&Route::GetMessage {
193    ///     channel_id: 1,
194    ///     message_id: 2,
195    /// });
196    /// ```
197    ///
198    /// [`builder`]: Self::builder
199    pub fn from_route(route: &Route<'_>) -> Self {
200        Self {
201            body: None,
202            form: None,
203            headers: None,
204            method: route.method(),
205            path: route.to_string(),
206            use_authorization_token: true,
207        }
208    }
209
210    /// Body of the request, if any.
211    pub fn body(&self) -> Option<&[u8]> {
212        self.body.as_deref()
213    }
214
215    /// Multipart form of the request, if any.
216    pub const fn form(&self) -> Option<&Form> {
217        self.form.as_ref()
218    }
219
220    /// Headers to set in the request, if any.
221    pub const fn headers(&self) -> Option<&HeaderMap<HeaderValue>> {
222        self.headers.as_ref()
223    }
224
225    /// Method when sending the request.
226    pub const fn method(&self) -> Method {
227        self.method
228    }
229
230    /// String path of the full URL.
231    #[allow(clippy::missing_const_for_fn)]
232    pub fn path(&self) -> &str {
233        &self.path
234    }
235
236    /// Whether to use the client's authorization token in the request.
237    pub const fn use_authorization_token(&self) -> bool {
238        self.use_authorization_token
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::RequestBuilder;
245    use static_assertions::assert_impl_all;
246    use std::fmt::Debug;
247
248    assert_impl_all!(RequestBuilder: Debug, Send, Sync);
249}