moosync_edk/
http.rs

1// Moosync
2// Copyright (C) 2024, 2025  Moosync <support@moosync.app>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17use crate::handler::MoosyncError;
18pub use extensions_proto::moosync::types::{
19    http_result, BatchHttpRequest, BatchHttpResponse, HttpRequest as ProtoHttpRequest,
20    HttpResponse as ProtoHttpResponse, HttpResult,
21};
22use extism_pdk::{host_fn, Prost};
23use std::collections::HashMap;
24
25#[host_fn]
26extern "ExtismHost" {
27    fn batch_http_request(req: Prost<BatchHttpRequest>) -> Prost<BatchHttpResponse>;
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct HttpRequest {
32    pub url: String,
33    pub method: String,
34    pub headers: HashMap<String, String>,
35    pub body: Option<Vec<u8>>,
36    pub timeout_ms: Option<u64>,
37}
38
39impl HttpRequest {
40    pub fn new<S: Into<String>>(url: S) -> Self {
41        Self {
42            url: url.into(),
43            method: "GET".to_string(),
44            headers: HashMap::new(),
45            body: None,
46            timeout_ms: None,
47        }
48    }
49
50    pub fn get<S: Into<String>>(url: S) -> Self {
51        Self::new(url)
52    }
53
54    pub fn post<S: Into<String>>(url: S) -> Self {
55        Self {
56            url: url.into(),
57            method: "POST".to_string(),
58            headers: HashMap::new(),
59            body: None,
60            timeout_ms: None,
61        }
62    }
63
64    pub fn method<S: Into<String>>(mut self, method: S) -> Self {
65        self.method = method.into();
66        self
67    }
68
69    pub fn header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
70        self.headers.insert(key.into(), value.into());
71        self
72    }
73
74    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
75        self.headers.extend(headers);
76        self
77    }
78
79    pub fn body<B: Into<Vec<u8>>>(mut self, body: B) -> Self {
80        self.body = Some(body.into());
81        self
82    }
83
84    pub fn json<T: serde::Serialize>(mut self, data: &T) -> Result<Self, serde_json::Error> {
85        let bytes = serde_json::to_vec(data)?;
86        self.headers
87            .insert("Content-Type".to_string(), "application/json".to_string());
88        self.body = Some(bytes);
89        Ok(self)
90    }
91
92    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
93        self.timeout_ms = Some(timeout_ms);
94        self
95    }
96}
97
98impl From<HttpRequest> for ProtoHttpRequest {
99    fn from(req: HttpRequest) -> Self {
100        ProtoHttpRequest {
101            url: req.url,
102            method: req.method,
103            headers: req.headers,
104            body: req.body,
105            timeout_ms: req.timeout_ms,
106        }
107    }
108}
109
110impl From<&HttpRequest> for ProtoHttpRequest {
111    fn from(req: &HttpRequest) -> Self {
112        ProtoHttpRequest {
113            url: req.url.clone(),
114            method: req.method.clone(),
115            headers: req.headers.clone(),
116            body: req.body.clone(),
117            timeout_ms: req.timeout_ms,
118        }
119    }
120}
121
122#[derive(Debug, Clone, Default)]
123pub struct HttpResponse {
124    pub status_code: u32,
125    pub status_text: String,
126    pub headers: HashMap<String, String>,
127    pub body: Vec<u8>,
128}
129
130impl HttpResponse {
131    pub fn is_success(&self) -> bool {
132        self.status_code >= 200 && self.status_code < 300
133    }
134
135    pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
136        String::from_utf8(self.body.clone())
137    }
138
139    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
140        serde_json::from_slice(&self.body)
141    }
142}
143
144impl From<ProtoHttpResponse> for HttpResponse {
145    fn from(resp: ProtoHttpResponse) -> Self {
146        HttpResponse {
147            status_code: resp.status_code,
148            status_text: resp.status_text,
149            headers: resp.headers,
150            body: resp.body,
151        }
152    }
153}
154
155/// Executes a single HTTP request on the host runner.
156pub fn request(request: &HttpRequest) -> Result<HttpResponse, MoosyncError> {
157    let mut responses = batch_request(std::slice::from_ref(request))?;
158    let Some(resp) = responses.pop() else {
159        return Err(MoosyncError::String(
160            "Host runner returned empty response".to_string(),
161        ));
162    };
163    Ok(resp)
164}
165
166/// Convenience helper to perform a GET request for a URL.
167pub fn get<S: AsRef<str>>(
168    url: S,
169    headers: Option<&HashMap<String, String>>,
170) -> Result<HttpResponse, MoosyncError> {
171    let mut req = HttpRequest::get(url.as_ref());
172    if let Some(h) = headers {
173        req = req.headers(h.clone());
174    }
175    request(&req)
176}
177
178/// Executes multiple HTTP requests concurrently in parallel on the host.
179/// Returns Ok only if all requests succeed; returns Err if even one request fails.
180/// Failure and Success is in terms of Moosync's validation
181/// and not HTTP status codes
182pub fn batch_request(requests: &[HttpRequest]) -> Result<Vec<HttpResponse>, MoosyncError> {
183    if requests.is_empty() {
184        return Ok(Vec::new());
185    }
186    let proto_requests: Vec<ProtoHttpRequest> = requests.iter().map(Into::into).collect();
187    let batch = BatchHttpRequest {
188        requests: proto_requests,
189    };
190    let res = unsafe { batch_http_request(Prost(batch)) }
191        .map_err(|e| MoosyncError::String(format!("batch_http_request failed: {e:?}")))?;
192
193    if let Some(err) = res.0.error {
194        return Err(MoosyncError::String(err));
195    }
196
197    let mut results = Vec::new();
198    for (idx, item) in res.0.responses.into_iter().enumerate() {
199        match item.result {
200            Some(http_result::Result::Response(r)) => {
201                results.push(r.into());
202            }
203            Some(http_result::Result::Error(err)) => {
204                return Err(MoosyncError::String(format!(
205                    "Request #{idx} failed: {err}"
206                )));
207            }
208            None => {
209                return Err(MoosyncError::String(format!(
210                    "Request #{idx} returned empty HTTP result"
211                )));
212            }
213        }
214    }
215    Ok(results)
216}
217
218/// Convenience helper to perform parallel GET requests for a list of URLs.
219pub fn batch_get<S: AsRef<str>>(
220    urls: &[S],
221    headers: Option<&HashMap<String, String>>,
222) -> Result<Vec<HttpResponse>, MoosyncError> {
223    let requests: Vec<HttpRequest> = urls
224        .iter()
225        .map(|u| {
226            let mut req = HttpRequest::get(u.as_ref());
227            if let Some(h) = headers {
228                req = req.headers(h.clone());
229            }
230            req
231        })
232        .collect();
233    batch_request(&requests)
234}