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