use std::sync::Arc;
use super::{MapKey, Value};
pub trait HttpBackend {
fn get(&self, url: &str, headers: &[(String, String)]) -> Result<String, String>;
fn post(&self, url: &str, body: &str, headers: &[(String, String)]) -> Result<String, String>;
fn get_stream(
&self,
url: &str,
headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>;
fn post_stream(
&self,
url: &str,
body: &str,
headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>;
}
#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
pub struct NativeHttpBackend;
#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
impl HttpBackend for NativeHttpBackend {
fn get(&self, url: &str, headers: &[(String, String)]) -> Result<String, String> {
let mut req = minreq::get(url);
for (k, v) in headers {
req = req.with_header(k.as_str(), v.as_str());
}
req.send()
.map_err(|e| e.to_string())
.and_then(|r| r.as_str().map(|s| s.to_owned()).map_err(|e| e.to_string()))
}
fn post(&self, url: &str, body: &str, headers: &[(String, String)]) -> Result<String, String> {
let mut req = minreq::post(url).with_body(body);
for (k, v) in headers {
req = req.with_header(k.as_str(), v.as_str());
}
req.send()
.map_err(|e| e.to_string())
.and_then(|r| r.as_str().map(|s| s.to_owned()).map_err(|e| e.to_string()))
}
fn get_stream(
&self,
url: &str,
headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
let mut req = minreq::get(url);
for (k, v) in headers {
req = req.with_header(k.as_str(), v.as_str());
}
let resp = req.send_lazy().map_err(|e| e.to_string())?;
use std::io::BufRead;
let reader = std::io::BufReader::new(resp);
Ok(Box::new(reader.lines()))
}
fn post_stream(
&self,
url: &str,
body: &str,
headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
let mut req = minreq::post(url).with_body(body);
for (k, v) in headers {
req = req.with_header(k.as_str(), v.as_str());
}
let resp = req.send_lazy().map_err(|e| e.to_string())?;
use std::io::BufRead;
let reader = std::io::BufReader::new(resp);
Ok(Box::new(reader.lines()))
}
}
#[cfg(target_arch = "wasm32")]
pub struct WasmFetchBackend;
#[cfg(target_arch = "wasm32")]
#[link(wasm_import_module = "ilo_http")]
unsafe extern "C" {
fn ilo_http_get(url_ptr: *const u8, url_len: usize, hdr_ptr: *const u8, hdr_len: usize) -> u32;
fn ilo_http_post(
url_ptr: *const u8,
url_len: usize,
body_ptr: *const u8,
body_len: usize,
hdr_ptr: *const u8,
hdr_len: usize,
) -> u32;
fn ilo_http_response_status(handle: u32) -> u32;
fn ilo_http_response_body_len(handle: u32) -> u32;
fn ilo_http_response_body_read(handle: u32, buf_ptr: *mut u8, buf_len: usize) -> u32;
fn ilo_http_response_free(handle: u32);
}
#[cfg(target_arch = "wasm32")]
fn encode_headers(headers: &[(String, String)]) -> Vec<u8> {
let mut buf = Vec::new();
for (k, v) in headers {
buf.extend_from_slice(k.as_bytes());
buf.push(0);
buf.extend_from_slice(v.as_bytes());
buf.push(0);
}
buf
}
#[cfg(target_arch = "wasm32")]
impl HttpBackend for WasmFetchBackend {
fn get(&self, url: &str, headers: &[(String, String)]) -> Result<String, String> {
let hdr_buf = encode_headers(headers);
let handle =
unsafe { ilo_http_get(url.as_ptr(), url.len(), hdr_buf.as_ptr(), hdr_buf.len()) };
read_response(handle)
}
fn post(&self, url: &str, body: &str, headers: &[(String, String)]) -> Result<String, String> {
let hdr_buf = encode_headers(headers);
let handle = unsafe {
ilo_http_post(
url.as_ptr(),
url.len(),
body.as_ptr(),
body.len(),
hdr_buf.as_ptr(),
hdr_buf.len(),
)
};
read_response(handle)
}
fn get_stream(
&self,
_url: &str,
_headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
Err("HTTP streaming not supported on this build".to_string())
}
fn post_stream(
&self,
_url: &str,
_body: &str,
_headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
Err("HTTP streaming not supported on this build".to_string())
}
}
#[cfg(target_arch = "wasm32")]
fn read_response(handle: u32) -> Result<String, String> {
let body_len = unsafe { ilo_http_response_body_len(handle) } as usize;
let mut buf = vec![0u8; body_len];
if body_len > 0 {
unsafe { ilo_http_response_body_read(handle, buf.as_mut_ptr(), buf.len()) };
}
let status = unsafe { ilo_http_response_status(handle) };
unsafe { ilo_http_response_free(handle) };
let text = String::from_utf8(buf).map_err(|e| format!("response is not valid UTF-8: {e}"))?;
if handle == 0 || status == 0 {
Err(text)
} else {
Ok(text)
}
}
pub struct StubHttpBackend;
impl HttpBackend for StubHttpBackend {
fn get(&self, _url: &str, _headers: &[(String, String)]) -> Result<String, String> {
Err("http feature not enabled".to_string())
}
fn post(
&self,
_url: &str,
_body: &str,
_headers: &[(String, String)],
) -> Result<String, String> {
Err("http feature not enabled".to_string())
}
fn get_stream(
&self,
_url: &str,
_headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
Err("HTTP streaming not supported on this build".to_string())
}
fn post_stream(
&self,
_url: &str,
_body: &str,
_headers: &[(String, String)],
) -> Result<Box<dyn Iterator<Item = std::result::Result<String, std::io::Error>> + Send>, String>
{
Err("HTTP streaming not supported on this build".to_string())
}
}
pub fn default_backend() -> Box<dyn HttpBackend> {
#[cfg(target_arch = "wasm32")]
{
Box::new(WasmFetchBackend)
}
#[cfg(all(feature = "http", not(target_arch = "wasm32")))]
{
Box::new(NativeHttpBackend)
}
#[cfg(all(not(feature = "http"), not(target_arch = "wasm32")))]
{
Box::new(StubHttpBackend)
}
}
pub fn result_to_value(r: Result<String, String>) -> Value {
match r {
Ok(body) => Value::Ok(Box::new(Value::Text(Arc::new(body)))),
Err(msg) => Value::Err(Box::new(Value::Text(Arc::new(msg)))),
}
}
pub fn map_to_headers(map: &std::collections::HashMap<MapKey, Value>) -> Vec<(String, String)> {
map.iter()
.map(|(k, v)| {
let key = k.to_display_string();
let val = match v {
Value::Text(s) => (**s).clone(),
other => format!("{other:?}"),
};
(key, val)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stub_get_returns_err() {
let b = StubHttpBackend;
let r = b.get("https://example.com", &[]);
assert!(r.is_err());
assert!(r.unwrap_err().contains("http feature not enabled"));
}
#[test]
fn stub_post_returns_err() {
let b = StubHttpBackend;
let r = b.post("https://example.com", "body", &[]);
assert!(r.is_err());
}
#[test]
fn result_to_value_ok() {
let v = result_to_value(Ok("hello".to_string()));
assert!(matches!(v, Value::Ok(_)));
}
#[test]
fn result_to_value_err() {
let v = result_to_value(Err("boom".to_string()));
assert!(matches!(v, Value::Err(_)));
}
#[cfg(target_arch = "wasm32")]
#[test]
fn encode_headers_empty() {
assert!(encode_headers(&[]).is_empty());
}
#[cfg(target_arch = "wasm32")]
#[test]
fn encode_headers_one_pair() {
let enc = encode_headers(&[("Content-Type".to_string(), "application/json".to_string())]);
let expected = b"Content-Type\x00application/json\x00";
assert_eq!(&enc, expected);
}
}