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}