1use crate::headers::ContentType;
3use crate::Error;
4use core::fmt::Write as _;
5use embedded_io::Error as _;
6use embedded_io_async::Write;
7use heapless::String;
8
9pub struct Request<'req, B>
11where
12 B: RequestBody,
13{
14 pub(crate) method: Method,
15 pub(crate) base_path: Option<&'req str>,
16 pub(crate) path: &'req str,
17 pub(crate) auth: Option<Auth<'req>>,
18 pub(crate) host: Option<&'req str>,
19 pub(crate) body: Option<B>,
20 pub(crate) content_type: Option<ContentType>,
21 pub(crate) extra_headers: Option<&'req [(&'req str, &'req str)]>,
22}
23
24impl Default for Request<'_, ()> {
25 fn default() -> Self {
26 Self {
27 method: Method::GET,
28 base_path: None,
29 path: "/",
30 auth: None,
31 host: None,
32 body: None,
33 content_type: None,
34 extra_headers: None,
35 }
36 }
37}
38
39pub trait RequestBuilder<'req, B>
41where
42 B: RequestBody,
43{
44 type WithBody<T: RequestBody>: RequestBuilder<'req, T>;
45
46 fn headers(self, headers: &'req [(&'req str, &'req str)]) -> Self;
48 fn path(self, path: &'req str) -> Self;
50 fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T>;
52 fn host(self, host: &'req str) -> Self;
54 fn content_type(self, content_type: ContentType) -> Self;
56 fn basic_auth(self, username: &'req str, password: &'req str) -> Self;
58 fn build(self) -> Request<'req, B>;
60}
61
62pub enum Auth<'a> {
64 Basic { username: &'a str, password: &'a str },
65}
66
67impl<'req> Request<'req, ()> {
68 #[allow(clippy::new_ret_no_self)]
70 pub fn new(method: Method, path: &'req str) -> DefaultRequestBuilder<'req, ()> {
71 DefaultRequestBuilder(Request {
72 method,
73 path,
74 ..Default::default()
75 })
76 }
77
78 pub fn get(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
80 Self::new(Method::GET, path)
81 }
82
83 pub fn post(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
85 Self::new(Method::POST, path)
86 }
87
88 pub fn put(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
90 Self::new(Method::PUT, path)
91 }
92
93 pub fn delete(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
95 Self::new(Method::DELETE, path)
96 }
97
98 pub fn head(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
100 Self::new(Method::HEAD, path)
101 }
102}
103
104impl<'req, B> Request<'req, B>
105where
106 B: RequestBody,
107{
108 pub async fn write_header<C>(&self, c: &mut C) -> Result<(), Error>
110 where
111 C: Write,
112 {
113 write_str(c, self.method.as_str()).await?;
114 write_str(c, " ").await?;
115 if let Some(base_path) = self.base_path {
116 write_str(c, base_path.trim_end_matches('/')).await?;
117 if !self.path.starts_with('/') {
118 write_str(c, "/").await?;
119 }
120 }
121 write_str(c, self.path).await?;
122 write_str(c, " HTTP/1.1\r\n").await?;
123
124 if let Some(auth) = &self.auth {
125 match auth {
126 Auth::Basic { username, password } => {
127 use base64::engine::{general_purpose, Engine as _};
128
129 let mut combined: String<128> = String::new();
130 write!(combined, "{}:{}", username, password).map_err(|_| Error::Codec)?;
131 let mut authz = [0; 256];
132 let authz_len = general_purpose::STANDARD
133 .encode_slice(combined.as_bytes(), &mut authz)
134 .map_err(|_| Error::Codec)?;
135 write_str(c, "Authorization: Basic ").await?;
136 write_str(c, unsafe { core::str::from_utf8_unchecked(&authz[..authz_len]) }).await?;
137 write_str(c, "\r\n").await?;
138 }
139 }
140 }
141 if let Some(host) = &self.host {
142 write_header(c, "Host", host).await?;
143 }
144 if let Some(content_type) = &self.content_type {
145 write_header(c, "Content-Type", content_type.as_str()).await?;
146 }
147 if let Some(body) = self.body.as_ref() {
148 if let Some(len) = body.len() {
149 let mut s: String<32> = String::new();
150 write!(s, "{}", len).map_err(|_| Error::Codec)?;
151 write_header(c, "Content-Length", s.as_str()).await?;
152 } else {
153 write_header(c, "Transfer-Encoding", "chunked").await?;
154 }
155 }
156 if let Some(extra_headers) = self.extra_headers {
157 for (header, value) in extra_headers.iter() {
158 write_header(c, header, value).await?;
159 }
160 }
161 write_str(c, "\r\n").await?;
162 trace!("Header written");
163 Ok(())
164 }
165}
166
167pub struct DefaultRequestBuilder<'req, B>(Request<'req, B>)
168where
169 B: RequestBody;
170
171impl<'req, B> RequestBuilder<'req, B> for DefaultRequestBuilder<'req, B>
172where
173 B: RequestBody,
174{
175 type WithBody<T: RequestBody> = DefaultRequestBuilder<'req, T>;
176
177 fn headers(mut self, headers: &'req [(&'req str, &'req str)]) -> Self {
178 self.0.extra_headers.replace(headers);
179 self
180 }
181
182 fn path(mut self, path: &'req str) -> Self {
183 self.0.path = path;
184 self
185 }
186
187 fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T> {
188 DefaultRequestBuilder(Request {
189 method: self.0.method,
190 base_path: self.0.base_path,
191 path: self.0.path,
192 auth: self.0.auth,
193 host: self.0.host,
194 body: Some(body),
195 content_type: self.0.content_type,
196 extra_headers: self.0.extra_headers,
197 })
198 }
199
200 fn host(mut self, host: &'req str) -> Self {
201 self.0.host.replace(host);
202 self
203 }
204
205 fn content_type(mut self, content_type: ContentType) -> Self {
206 self.0.content_type.replace(content_type);
207 self
208 }
209
210 fn basic_auth(mut self, username: &'req str, password: &'req str) -> Self {
211 self.0.auth.replace(Auth::Basic { username, password });
212 self
213 }
214
215 fn build(self) -> Request<'req, B> {
216 self.0
217 }
218}
219
220#[derive(Clone, Copy, Debug, PartialEq)]
221#[cfg_attr(feature = "defmt", derive(defmt::Format))]
222pub enum Method {
224 GET,
226 PUT,
228 POST,
230 DELETE,
232 HEAD,
234}
235
236impl Method {
237 pub fn as_str(&self) -> &str {
239 match self {
240 Method::POST => "POST",
241 Method::PUT => "PUT",
242 Method::GET => "GET",
243 Method::DELETE => "DELETE",
244 Method::HEAD => "HEAD",
245 }
246 }
247}
248
249async fn write_str<C: Write>(c: &mut C, data: &str) -> Result<(), Error> {
250 c.write_all(data.as_bytes()).await.map_err(|e| e.kind())?;
251 Ok(())
252}
253
254async fn write_header<C: Write>(c: &mut C, key: &str, value: &str) -> Result<(), Error> {
255 write_str(c, key).await?;
256 write_str(c, ": ").await?;
257 write_str(c, value).await?;
258 write_str(c, "\r\n").await?;
259 Ok(())
260}
261
262#[allow(clippy::len_without_is_empty)]
264pub trait RequestBody {
265 fn len(&self) -> Option<usize> {
270 None
271 }
272
273 async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error>;
275}
276
277impl RequestBody for () {
278 fn len(&self) -> Option<usize> {
279 None
280 }
281
282 async fn write<W: Write>(&self, _writer: &mut W) -> Result<(), W::Error> {
283 Ok(())
284 }
285}
286
287impl RequestBody for &[u8] {
288 fn len(&self) -> Option<usize> {
289 Some(<[u8]>::len(self))
290 }
291
292 async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error> {
293 writer.write_all(self).await
294 }
295}
296
297impl<T> RequestBody for Option<T>
298where
299 T: RequestBody,
300{
301 fn len(&self) -> Option<usize> {
302 self.as_ref().map(|inner| inner.len()).unwrap_or_default()
303 }
304
305 async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error> {
306 if let Some(inner) = self.as_ref() {
307 inner.write(writer).await
308 } else {
309 Ok(())
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[tokio::test]
319 async fn basic_auth() {
320 let mut buffer: Vec<u8> = Vec::new();
321 Request::new(Method::GET, "/")
322 .basic_auth("username", "password")
323 .build()
324 .write_header(&mut buffer)
325 .await
326 .unwrap();
327
328 assert_eq!(
329 b"GET / HTTP/1.1\r\nAuthorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=\r\n\r\n",
330 buffer.as_slice()
331 );
332 }
333
334 #[tokio::test]
335 async fn with_empty_body() {
336 let mut buffer = Vec::new();
337 Request::new(Method::POST, "/")
338 .body([].as_slice())
339 .build()
340 .write_header(&mut buffer)
341 .await
342 .unwrap();
343
344 assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", buffer.as_slice());
345 }
346
347 #[tokio::test]
348 async fn with_known_body_adds_content_length_header() {
349 let mut buffer = Vec::new();
350 Request::new(Method::POST, "/")
351 .body(b"BODY".as_slice())
352 .build()
353 .write_header(&mut buffer)
354 .await
355 .unwrap();
356
357 assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\n", buffer.as_slice());
358 }
359
360 struct ChunkedBody;
361
362 impl RequestBody for ChunkedBody {
363 fn len(&self) -> Option<usize> {
364 None }
366
367 async fn write<W: Write>(&self, _writer: &mut W) -> Result<(), W::Error> {
368 unreachable!()
369 }
370 }
371
372 #[tokio::test]
373 async fn with_unknown_body_adds_transfer_encoding_header() {
374 let mut buffer = Vec::new();
375
376 Request::new(Method::POST, "/")
377 .body(ChunkedBody)
378 .build()
379 .write_header(&mut buffer)
380 .await
381 .unwrap();
382
383 assert_eq!(
384 b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n",
385 buffer.as_slice()
386 );
387 }
388}