Skip to main content

rustolio_utils/http/request/
builder.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11#[cfg(not(target_arch = "wasm32"))]
12use crate::bytes::Bytes;
13#[cfg(target_arch = "wasm32")]
14use js_sys::JsString;
15#[cfg(target_arch = "wasm32")]
16use wasm_bindgen::JsCast as _;
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::JsValue;
19#[cfg(target_arch = "wasm32")]
20use web_sys::RequestInit;
21
22use crate::bytes::{IntoUtf8Bytes, Utf8Bytes};
23
24use super::{Error, HeaderName, Incoming, Method, Outgoing, Request, Response, Result, Version};
25
26pub struct Builder<B> {
27    version: Version,
28    uri: Utf8Bytes,
29    method: Method,
30    headers: Vec<(Utf8Bytes, Utf8Bytes)>,
31    body: B,
32}
33
34impl Builder<Outgoing> {
35    pub fn new(method: Method, uri: Utf8Bytes) -> Self {
36        Builder {
37            version: Version::HTTP_11,
38            uri,
39            method,
40            headers: Vec::new(),
41            body: Outgoing::empty(),
42        }
43    }
44}
45
46impl Builder<()> {
47    pub fn new(method: Method, uri: Utf8Bytes) -> Self {
48        Builder {
49            version: Version::HTTP_11,
50            uri,
51            method,
52            headers: Vec::new(),
53            body: (),
54        }
55    }
56}
57
58impl<B> Builder<B> {
59    pub fn header(mut self, key: impl IntoUtf8Bytes, value: impl IntoUtf8Bytes) -> Self {
60        self.headers
61            .push((key.into_utf8_bytes(), value.into_utf8_bytes()));
62        self
63    }
64}
65
66#[cfg(target_arch = "wasm32")]
67impl Builder<()> {
68    pub fn bytes(mut self, body: &[u8]) -> Builder<Outgoing> {
69        self.headers.push((
70            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
71            "application/octet-stream".into_utf8_bytes(),
72        ));
73        let body = JsValue::from(js_sys::Uint8Array::from(body)).into();
74        Builder {
75            version: self.version,
76            uri: self.uri,
77            method: self.method,
78            headers: self.headers,
79            body,
80        }
81    }
82
83    pub fn text(mut self, body: &str) -> Builder<Outgoing> {
84        self.headers.push((
85            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
86            "text/html; charset=utf-8".into_utf8_bytes(),
87        ));
88        let body = JsValue::from(body).into();
89        Builder {
90            version: self.version,
91            uri: self.uri,
92            method: self.method,
93            headers: self.headers,
94            body,
95        }
96    }
97
98    pub fn encoded<T: crate::prelude::Encode>(self, body: &T) -> Result<Builder<Outgoing>> {
99        let encoded = crate::bytes::encoding::encode_to_bytes(body).map_err(|_| Error::Parse)?;
100        Ok(self.bytes(&encoded))
101    }
102
103    pub fn json<T>(mut self, body: T) -> Result<Builder<Outgoing>>
104    where
105        T: serde::ser::Serialize,
106    {
107        self.headers.push((
108            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
109            "application/json".into_utf8_bytes(),
110        ));
111        let js_obj = serde_wasm_bindgen::to_value(&body).map_err(|_| Error::Parse)?;
112        let js_string = js_sys::JSON::stringify(&js_obj).expect("Should always be valid json");
113        let body = JsValue::from(js_string).into();
114        Ok(Builder {
115            version: self.version,
116            uri: self.uri,
117            method: self.method,
118            headers: self.headers,
119            body,
120        })
121    }
122}
123
124#[cfg(not(target_arch = "wasm32"))]
125impl Builder<()> {
126    pub fn bytes(mut self, body: Bytes) -> Builder<Outgoing> {
127        self.headers.push((
128            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
129            "application/octet-stream".into_utf8_bytes(),
130        ));
131        let body = Outgoing::from_bytes(body);
132        Builder {
133            version: self.version,
134            uri: self.uri,
135            method: self.method,
136            headers: self.headers,
137            body,
138        }
139    }
140
141    pub fn text(mut self, body: impl IntoUtf8Bytes) -> Builder<Outgoing> {
142        self.headers.push((
143            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
144            "text/html; charset=utf-8".into_utf8_bytes(),
145        ));
146        let body = Outgoing::from_bytes(body.into());
147        Builder {
148            version: self.version,
149            uri: self.uri,
150            method: self.method,
151            headers: self.headers,
152            body,
153        }
154    }
155
156    // TODO: Use streaming instead of bytes
157    pub fn encoded<T: crate::prelude::Encode>(self, body: &T) -> Result<Builder<Outgoing>> {
158        let encoded = crate::bytes::encoding::encode_to_bytes(body).map_err(|_| Error::Parse)?;
159        Ok(self.bytes(encoded))
160    }
161
162    pub fn json<T>(mut self, body: &T) -> Result<Builder<Outgoing>>
163    where
164        T: serde::ser::Serialize,
165    {
166        let body = serde_json::to_vec(body).map_err(|_| Error::Parse)?;
167        self.headers.push((
168            HeaderName::CONTENT_TYPE.into_utf8_bytes(),
169            "application/json".into_utf8_bytes(),
170        ));
171        let body = Outgoing::from_bytes(Bytes::from(body));
172        Ok(Builder {
173            version: self.version,
174            uri: self.uri,
175            method: self.method,
176            headers: self.headers,
177            body,
178        })
179    }
180
181    pub fn body<B>(self, body: B) -> Builder<B> {
182        Builder {
183            version: self.version,
184            uri: self.uri,
185            method: self.method,
186            headers: self.headers,
187            body,
188        }
189    }
190}
191
192#[cfg(not(target_arch = "wasm32"))]
193impl<B> Builder<B> {
194    pub fn build(self) -> Result<Request<B>> {
195        let uri: http::Uri = self.uri.to_vec().try_into().map_err(http::Error::from)?;
196        let mut res = http::Request::builder()
197            .version(self.version)
198            .method(self.method)
199            .uri(uri)
200            .body(self.body)?;
201        let headers = res.headers_mut();
202        for (key, value) in self.headers {
203            let key: http::HeaderName = key.to_vec().try_into().map_err(http::Error::from)?;
204            let value: http::HeaderValue = value.to_vec().try_into().map_err(http::Error::from)?;
205            headers.append(key, value);
206        }
207        Ok(Request(res))
208    }
209}
210
211#[cfg(target_arch = "wasm32")]
212impl Builder<Outgoing> {
213    pub async fn fetch(self) -> Result<Response<Incoming>> {
214        let Builder {
215            uri,
216            method,
217            headers,
218            body,
219            ..
220        } = self;
221
222        let uri = str::from_utf8(&uri).map_err(|_| Error::InvalidUri)?;
223
224        let req = RequestInit::new();
225        req.set_method(method.as_str());
226        req.set_body(&body);
227        let req =
228            web_sys::Request::new_with_str_and_init(uri, &req).map_err(|_| Error::InvalidUri)?;
229
230        for (key, value) in headers {
231            req.headers().set(key.as_str(), value.as_str()).ok(); // Cannot fail
232        }
233
234        let window = web_sys::window().expect("No window available");
235        let response: web_sys::Response =
236            wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&req))
237                .await
238                .map_err(|_| Error::Fetch)?
239                .unchecked_into();
240
241        Ok(Response::from_inner(response))
242    }
243}
244
245#[cfg(not(target_arch = "wasm32"))]
246impl Builder<Outgoing> {
247    pub async fn fetch(self) -> Result<Response<Incoming>> {
248        todo!()
249    }
250}