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}