Skip to main content

aidoku/imports/
net.rs

1//! Module for creating and sending HTTP requests.
2use super::{
3	FFIResult, Rid,
4	canvas::ImageRef,
5	error::AidokuError,
6	html::Document,
7	std::{destroy, read_string_and_destroy},
8};
9use crate::alloc::{String, Vec};
10
11/// An HTTP request method.
12#[repr(C)]
13#[derive(PartialEq, Eq, Debug, Clone, Copy)]
14pub enum HttpMethod {
15	Get,
16	Post,
17	Put,
18	Head,
19	Delete,
20	Patch,
21	Options,
22	Connect,
23	Trace,
24}
25
26#[link(wasm_import_module = "net")]
27unsafe extern "C" {
28	fn init(method: HttpMethod) -> Rid;
29	fn send(rid: Rid) -> FFIResult;
30	fn send_all(rd: *mut Rid, len: usize) -> FFIResult;
31
32	fn set_url(rid: Rid, value: *const u8, len: usize) -> FFIResult;
33	fn set_header(
34		rid: Rid,
35		key: *const u8,
36		key_len: usize,
37		val: *const u8,
38		val_len: usize,
39	) -> FFIResult;
40	fn set_body(rid: Rid, value: *const u8, len: usize) -> FFIResult;
41	fn set_timeout(rid: Rid, value: f64) -> FFIResult;
42
43	fn data_len(rid: Rid) -> FFIResult;
44	fn read_data(rid: Rid, buffer: *mut u8, size: usize) -> FFIResult;
45	fn get_image(rid: Rid) -> FFIResult;
46	fn get_header(rid: Rid, key: *const u8, key_len: usize) -> FFIResult;
47	fn get_status_code(rid: Rid) -> FFIResult;
48	fn get_url(rid: Rid) -> FFIResult;
49	fn html(rid: Rid) -> FFIResult;
50
51	#[link_name = "set_rate_limit"]
52	fn net_set_rate_limit(permits: i32, period: i32, unit: i32);
53}
54
55/// A time unit for rate limiting.
56pub enum TimeUnit {
57	Seconds,
58	Minutes,
59	Hours,
60}
61
62impl From<TimeUnit> for i32 {
63	fn from(unit: TimeUnit) -> i32 {
64		match unit {
65			TimeUnit::Seconds => 0,
66			TimeUnit::Minutes => 1,
67			TimeUnit::Hours => 2,
68		}
69	}
70}
71
72/// Error type for network requests.
73#[derive(PartialEq, Eq, Debug, Clone, Copy)]
74pub enum RequestError {
75	InvalidDescriptor,
76	InvalidString,
77	InvalidMethod,
78	InvalidUrl,
79	InvalidHtml,
80	InvalidBufferSize,
81	MissingData,
82	MissingResponse,
83	MissingUrl,
84	RequestError,
85	FailedMemoryWrite,
86	NotAnImage,
87	Closed,
88}
89
90impl RequestError {
91	fn from(value: i32) -> Option<Self> {
92		match value {
93			-1 => Some(Self::InvalidDescriptor),
94			-2 => Some(Self::InvalidString),
95			-3 => Some(Self::InvalidMethod),
96			-4 => Some(Self::InvalidUrl),
97			-5 => Some(Self::InvalidHtml),
98			-6 => Some(Self::InvalidBufferSize),
99			-7 => Some(Self::MissingData),
100			-8 => Some(Self::MissingResponse),
101			-9 => Some(Self::MissingUrl),
102			-10 => Some(Self::RequestError),
103			-11 => Some(Self::FailedMemoryWrite),
104			-12 => Some(Self::NotAnImage),
105			_ => None,
106		}
107	}
108}
109
110/// Macro for generating convenience HTTP methods, e.g.
111/// Request::get, Request::post.
112#[doc(hidden)]
113macro_rules! convenience_http_methods {
114	($name:ident, $t:expr, $doc:tt) => {
115		#[inline]
116		#[doc = $doc]
117		pub fn $name<T: AsRef<str>>(url: T) -> Result<Self, RequestError> {
118			Self::new(url, $t)
119		}
120	};
121}
122
123/// An HTTP request.
124#[derive(Debug)]
125pub struct Request {
126	/// The reference id for the request.
127	///
128	/// This property is exposed for the functions that the [register_source](crate::register_source)
129	/// macro generates and should not be used directly.
130	pub rid: Rid,
131	http_method: HttpMethod,
132	url: Option<String>,
133	/// Whether the request should be closed after being dropped.
134	///
135	/// This property is exposed for the functions that the [register_source](crate::register_source)
136	/// macro generates and should not be used directly.
137	pub should_close: bool,
138}
139
140/// An HTTP response.
141#[derive(Debug)]
142pub struct Response {
143	rid: Rid,
144	http_method: HttpMethod,
145	url: Option<String>,
146	/// The stored request response data.
147	pub data: Option<Vec<u8>>,
148}
149
150impl Request {
151	/// Create a new request with a URL and HTTP method.
152	///
153	/// Returns an error if the provided URL is invalid.
154	///
155	/// # Examples
156	///
157	/// ```ignore
158	/// use aidoku::imports::net::{HttpMethod, Request};
159	/// Request::new("https://example.com", HttpMethod::Get).unwrap();
160	/// ```
161	pub fn new<T: AsRef<str>>(url: T, http_method: HttpMethod) -> Result<Self, RequestError> {
162		let url = url.as_ref();
163		unsafe {
164			let rid = init(http_method);
165			let mut request = Self {
166				rid,
167				http_method,
168				url: None,
169				should_close: true,
170			};
171			request.set_url(url)?;
172			Ok(request)
173		}
174	}
175
176	convenience_http_methods! { get, HttpMethod::Get, "Create a new GET request with the given URL." }
177	convenience_http_methods! { post, HttpMethod::Post, "Create a new POST request with the given URL." }
178	convenience_http_methods! { put, HttpMethod::Put, "Create a new PUT request with the given URL." }
179	convenience_http_methods! { head, HttpMethod::Head, "Create a new HEAD request with the given URL." }
180	convenience_http_methods! { delete, HttpMethod::Delete, "Create a new DELETE request with the given URL." }
181	convenience_http_methods! { patch, HttpMethod::Patch, "Create a new PATCH request with the given URL." }
182
183	/// Send multiple requests in parallel, and wait for all of them to finish.
184	pub fn send_all<I>(requests: I) -> Vec<Result<Response, RequestError>>
185	where
186		I: IntoIterator<Item = Request>,
187	{
188		let mut ids: Vec<i32> = Vec::new();
189		// mark all requests as sent and add their IDs to the vector
190		let responses = requests
191			.into_iter()
192			.map(|mut r| {
193				r.should_close = false;
194				ids.push(r.rid);
195				Ok(Response::from(r))
196			})
197			.collect();
198
199		let result = unsafe { send_all(ids.as_mut_ptr(), ids.len()) };
200
201		if result == 0 {
202			// success, return result
203			responses
204		} else {
205			// one or more of the requests failed
206			// the error codes are stored in the ids vector
207			let mut idx = 0;
208			responses
209				.into_iter()
210				.map(|response| {
211					let error = ids.get(idx).and_then(|id| RequestError::from(*id));
212					let result = match error {
213						Some(e) => Err(e),
214						None => response,
215					};
216					idx += 1;
217					result
218				})
219				.collect()
220		}
221	}
222
223	/// Set an HTTP header in a builder.
224	pub fn header<T: AsRef<str>>(mut self, key: T, val: T) -> Self {
225		self.set_header(key, val);
226		self
227	}
228
229	/// Set an HTTP header.
230	pub fn set_header<T: AsRef<str>>(&mut self, key: T, val: T) {
231		let key = key.as_ref();
232		let val = val.as_ref();
233		unsafe {
234			set_header(self.rid, key.as_ptr(), key.len(), val.as_ptr(), val.len());
235		};
236	}
237
238	/// Set the HTTP body data in a builder.
239	pub fn body<T: AsRef<[u8]>>(mut self, data: T) -> Self {
240		self.set_body(data);
241		self
242	}
243
244	/// Set the request timeout interval in a builder.
245	///
246	/// The request timeout interval controls how long (in seconds) a task
247	/// should wait for additional data to arrive before giving up.
248	pub fn timeout(mut self, value: f64) -> Self {
249		self.set_timeout(value);
250		self
251	}
252
253	/// Set the HTTP body data.
254	pub fn set_body<T: AsRef<[u8]>>(&mut self, data: T) {
255		let data = data.as_ref();
256		unsafe { set_body(self.rid, data.as_ptr(), data.len()) };
257	}
258
259	/// Set the request timeout interval.
260	///
261	/// The request timeout interval controls how long (in seconds) a task
262	/// should wait for additional data to arrive before giving up.
263	pub fn set_timeout(&mut self, value: f64) {
264		unsafe { set_timeout(self.rid, value) };
265	}
266
267	/// Set the URL for the request.
268	pub fn set_url<T: AsRef<str>>(&mut self, url: T) -> Result<(), RequestError> {
269		let url = url.as_ref();
270		self.url = Some(String::from(url));
271		let result = unsafe { set_url(self.rid, url.as_ptr(), url.len()) };
272		if let Some(error) = RequestError::from(result) {
273			Err(error)
274		} else {
275			Ok(())
276		}
277	}
278
279	/// Get the URL of the request.
280	pub fn url(&self) -> Option<&String> {
281		self.url.as_ref()
282	}
283
284	/// Send the request.
285	#[inline]
286	pub fn send(mut self) -> Result<Response, RequestError> {
287		let result = unsafe { send(self.rid) };
288		if let Some(error) = RequestError::from(result) {
289			Err(error)
290		} else {
291			self.should_close = false;
292			Ok(Response::from(self))
293		}
294	}
295
296	/// Get the raw data from the response, closing the request.
297	pub fn data(self) -> Result<Vec<u8>, RequestError> {
298		self.send()?.get_data()
299	}
300
301	/// Gets the response data as an image.
302	pub fn image(self) -> Result<ImageRef, RequestError> {
303		self.send()?.get_image()
304	}
305
306	/// Gets the response data as a string.
307	pub fn string(self) -> Result<String, AidokuError> {
308		self.send()?.get_string()
309	}
310
311	/// Get the response data as an HTML [Document].
312	pub fn html(self) -> Result<Document, RequestError> {
313		self.send()?.get_html()
314	}
315}
316
317#[cfg(feature = "json")]
318impl Request {
319	/// Get the response data as an owned JSON value.
320	pub fn json_owned<T>(self) -> Result<T, AidokuError>
321	where
322		T: serde::de::DeserializeOwned,
323	{
324		self.send()?.get_json_owned()
325	}
326}
327
328impl Response {
329	/// Get the response's status code.
330	#[inline]
331	pub fn status_code(&self) -> i32 {
332		unsafe { get_status_code(self.rid) }
333	}
334
335	/// Get the final URL for the response.
336	pub fn get_url(&self) -> Option<String> {
337		let rid = unsafe { get_url(self.rid) };
338		if rid < 0 {
339			return None;
340		}
341		read_string_and_destroy(rid)
342	}
343
344	/// Get a response HTTP header.
345	pub fn get_header<T: AsRef<str>>(&self, header: T) -> Option<String> {
346		let header = header.as_ref();
347		let rid = unsafe { get_header(self.rid, header.as_ptr(), header.len()) };
348		if rid < 0 {
349			return None;
350		}
351		read_string_and_destroy(rid)
352	}
353
354	/// Get the raw data from the response.
355	pub fn get_data(&self) -> Result<Vec<u8>, RequestError> {
356		let size = unsafe { data_len(self.rid) };
357		if let Some(error) = RequestError::from(size) {
358			return Err(error);
359		}
360		let size = size as usize;
361		let mut buffer: Vec<u8> = Vec::with_capacity(size);
362		unsafe {
363			let result = read_data(self.rid, buffer.as_mut_ptr(), size);
364			if let Some(error) = RequestError::from(result) {
365				return Err(error);
366			}
367			buffer.set_len(size);
368		}
369		Ok(buffer)
370	}
371
372	/// Gets the response data as an image.
373	pub fn get_image(&self) -> Result<ImageRef, RequestError> {
374		let result = unsafe { get_image(self.rid) };
375		if let Some(error) = RequestError::from(result) {
376			Err(error)
377		} else {
378			Ok(ImageRef::from(result, false))
379		}
380	}
381
382	/// Gets the response data as a string.
383	pub fn get_string(&self) -> Result<String, AidokuError> {
384		let res = String::from_utf8(self.get_data()?);
385		match res {
386			Ok(res) => Ok(res),
387			Err(err) => Err(AidokuError::Utf8Error(err.utf8_error())),
388		}
389	}
390
391	/// Get the response data as an HTML [Document].
392	pub fn get_html(&self) -> Result<Document, RequestError> {
393		let rid = unsafe { html(self.rid) };
394		if let Some(error) = RequestError::from(rid) {
395			return Err(error);
396		}
397		Ok(unsafe { Document::from(rid) })
398	}
399
400	/// Create a new request with the same method and url as the one sent to get this response.
401	pub fn into_request(self) -> Request {
402		let rid = unsafe { init(self.http_method) };
403		if let Some(url) = self.url.as_ref() {
404			_ = unsafe { set_url(rid, url.as_ptr(), url.len()) };
405		}
406		Request {
407			rid,
408			http_method: self.http_method,
409			url: self.url.clone(),
410			should_close: true,
411		}
412	}
413}
414
415#[cfg(feature = "json")]
416impl Response {
417	/// Get the response data as a JSON value. This requires the request to stay in scope so the data can be referenced.
418	pub fn get_json<'a, T>(&'a mut self) -> Result<T, AidokuError>
419	where
420		T: serde::de::Deserialize<'a>,
421	{
422		let data = self.get_data()?;
423		self.data = Some(data);
424		let value = serde_json::from_slice(self.data.as_ref().unwrap())?;
425		Ok(value)
426	}
427
428	/// Get the response data as an owned JSON value.
429	pub fn get_json_owned<T>(self) -> Result<T, AidokuError>
430	where
431		T: serde::de::DeserializeOwned,
432	{
433		let data = self.get_data()?;
434		let value = serde_json::from_slice(&data)?;
435		Ok(value)
436	}
437}
438
439impl Response {
440	// don't implement From<Request> here since this should stay private
441	fn from(request: Request) -> Self {
442		Self {
443			rid: request.rid,
444			http_method: request.http_method,
445			url: request.url.clone(),
446			data: None,
447		}
448	}
449}
450
451impl Drop for Request {
452	fn drop(&mut self) {
453		if self.should_close {
454			unsafe { destroy(self.rid) };
455		}
456	}
457}
458
459impl Drop for Response {
460	fn drop(&mut self) {
461		unsafe { destroy(self.rid) };
462	}
463}
464
465/// Set the number of requests allowed per a given time period.
466///
467/// For example, `set_rate_limit(10, 60, TimeUnit::Seconds)` will allow 10 requests per minute.
468/// If a request is made while the limit is exceeded, the request will be queued and executed
469/// once the period is complete.
470pub fn set_rate_limit(permits: i32, period: i32, unit: TimeUnit) {
471	unsafe { net_set_rate_limit(permits, period, unit.into()) }
472}