1use 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
155pub 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
166pub 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
178pub 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
218pub 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}