1use std::{collections::BTreeMap, io::Read, ops::Deref, time::Duration};
18use bytes::Bytes;
19use stof::{lang::SError, Library, SDoc, SNodeRef, SUnits, SVal};
20use ureq::Agent;
21
22pub mod server;
23
24#[derive(Debug)]
25pub struct HTTPLibrary {
26 pub agent: Agent,
27}
28impl Default for HTTPLibrary {
29 fn default() -> Self {
30 Self {
31 agent: ureq::AgentBuilder::new()
32 .timeout_read(Duration::from_secs(5))
33 .timeout_write(Duration::from_secs(5))
34 .build(),
35 }
36 }
37}
38impl Library for HTTPLibrary {
39 fn scope(&self) -> String {
43 "HTTP".to_string()
44 }
45
46 fn call(&self, pid: &str, doc: &mut SDoc, name: &str, parameters: &mut Vec<SVal>) -> Result<SVal, SError> {
69 let url;
70 if parameters.len() > 0 {
71 match ¶meters[0] {
72 SVal::String(val) => {
73 url = val.clone();
74 },
75 SVal::Boxed(val) => {
76 let val = val.lock().unwrap();
77 let val = val.deref();
78 match val {
79 SVal::String(val) => {
80 url = val.clone();
81 },
82 _ => {
83 return Err(SError::custom(pid, &doc, "HTTPError", "url must be a string"));
84 }
85 }
86 },
87 _ => {
88 return Err(SError::custom(pid, &doc, "HTTPError", "url must be a string"));
89 }
90 }
91 } else {
92 return Err(SError::custom(pid, &doc, "HTTPError", "must provide a URL as the first parameter when calling into the HTTP library"));
93 }
94
95 let mut request;
96 match name {
97 "get" => request = self.agent.get(&url),
98 "head" => request = self.agent.head(&url),
99 "patch" => request = self.agent.patch(&url),
100 "post" => request = self.agent.post(&url),
101 "put" => request = self.agent.put(&url),
102 "delete" => request = self.agent.delete(&url),
103 _ => {
104 return Err(SError::custom(pid, &doc, "HTTPError", &format!("unrecognized HTTP library function: {}", name)));
105 }
106 }
107
108 let mut headers = Vec::new();
109 let mut str_body: Option<String> = None;
110 let mut blob_body: Option<Vec<u8>> = None;
111 let mut timeout = Duration::from_secs(5);
112 let mut response_obj: Option<SNodeRef> = None;
113 if parameters.len() > 1 {
114 match ¶meters[1] {
115 SVal::Array(vals) => {
116 for val in vals {
117 match val {
118 SVal::Tuple(vals) => {
119 if vals.len() == 2 {
120 headers.push((vals[0].to_string(), vals[1].to_string()));
121 }
122 },
123 _ => {}
124 }
125 }
126 },
127 SVal::Map(map) => {
128 for (k, v) in map {
129 headers.push((k.to_string(), v.to_string()));
130 }
131 },
132 SVal::String(body) => {
133 str_body = Some(body.clone());
134 },
135 SVal::Blob(body) => {
136 blob_body = Some(body.clone());
137 },
138 SVal::Number(num) => {
139 let seconds = num.float_with_units(SUnits::Seconds);
140 timeout = Duration::from_secs(seconds as u64);
141 },
142 SVal::Object(nref) => {
143 response_obj = Some(nref.clone());
144 },
145 SVal::Boxed(val) => {
146 let val = val.lock().unwrap();
147 let val = val.deref();
148 match val {
149 SVal::Array(vals) => {
150 for val in vals {
151 match val {
152 SVal::Tuple(vals) => {
153 if vals.len() == 2 {
154 headers.push((vals[0].to_string(), vals[1].to_string()));
155 }
156 },
157 _ => {}
158 }
159 }
160 },
161 SVal::Map(map) => {
162 for (k, v) in map {
163 headers.push((k.to_string(), v.to_string()));
164 }
165 },
166 SVal::String(body) => {
167 str_body = Some(body.clone());
168 },
169 SVal::Blob(body) => {
170 blob_body = Some(body.clone());
171 },
172 SVal::Number(num) => {
173 let seconds = num.float_with_units(SUnits::Seconds);
174 timeout = Duration::from_secs(seconds as u64);
175 },
176 SVal::Object(nref) => {
177 response_obj = Some(nref.clone());
178 },
179 _ => {
180 return Err(SError::custom(pid, &doc, "HTTPError", "second parameter for an HTTP request must be either headers (vec), a body (str | blob), a timeout (float | units), or response object (obj)"));
181 }
182 }
183 },
184 _ => {
185 return Err(SError::custom(pid, &doc, "HTTPError", "second parameter for an HTTP request must be either headers (vec), a body (str | blob), a timeout (float | units), or response object (obj)"));
186 }
187 }
188 }
189 if parameters.len() > 2 {
190 match ¶meters[2] {
191 SVal::String(body) => {
192 str_body = Some(body.clone());
193 },
194 SVal::Blob(body) => {
195 blob_body = Some(body.clone());
196 },
197 SVal::Number(num) => {
198 let seconds = num.float_with_units(SUnits::Seconds);
199 timeout = Duration::from_secs(seconds as u64);
200 },
201 SVal::Object(nref) => {
202 response_obj = Some(nref.clone());
203 },
204 SVal::Boxed(val) => {
205 let val = val.lock().unwrap();
206 let val = val.deref();
207 match val {
208 SVal::String(body) => {
209 str_body = Some(body.clone());
210 },
211 SVal::Blob(body) => {
212 blob_body = Some(body.clone());
213 },
214 SVal::Number(num) => {
215 let seconds = num.float_with_units(SUnits::Seconds);
216 timeout = Duration::from_secs(seconds as u64);
217 },
218 SVal::Object(nref) => {
219 response_obj = Some(nref.clone());
220 },
221 _ => {
222 return Err(SError::custom(pid, &doc, "HTTPError", "third parameter for an HTTP request must be either a body (str | blob), a timeout (float | units), or a response object (obj)"));
223 }
224 }
225 },
226 _ => {
227 return Err(SError::custom(pid, &doc, "HTTPError", "third parameter for an HTTP request must be either a body (str | blob), a timeout (float | units), or a response object (obj)"));
228 }
229 }
230 }
231 if parameters.len() > 3 {
232 match ¶meters[3] {
233 SVal::Number(num) => {
234 let seconds = num.float_with_units(SUnits::Seconds);
235 timeout = Duration::from_secs(seconds as u64);
236 },
237 SVal::Object(nref) => {
238 response_obj = Some(nref.clone());
239 },
240 SVal::Boxed(val) => {
241 let val = val.lock().unwrap();
242 let val = val.deref();
243 match val {
244 SVal::Number(num) => {
245 let seconds = num.float_with_units(SUnits::Seconds);
246 timeout = Duration::from_secs(seconds as u64);
247 },
248 SVal::Object(nref) => {
249 response_obj = Some(nref.clone());
250 },
251 _ => {
252 return Err(SError::custom(pid, &doc, "HTTPError", "fourth parameter for an HTTP request must be a timeout (float | units) or a response object (obj)"));
253 }
254 }
255 },
256 _ => {
257 return Err(SError::custom(pid, &doc, "HTTPError", "fourth parameter for an HTTP request must be a timeout (float | units) or a response object (obj)"));
258 }
259 }
260 }
261 if parameters.len() > 4 {
262 match ¶meters[4] {
263 SVal::Object(nref) => {
264 response_obj = Some(nref.clone());
265 },
266 SVal::Boxed(val) => {
267 let val = val.lock().unwrap();
268 let val = val.deref();
269 match val {
270 SVal::Object(nref) => {
271 response_obj = Some(nref.clone());
272 },
273 _ => {
274 return Err(SError::custom(pid, &doc, "HTTPError", "fifth parameter for an HTTP request must be a response object (obj)"));
275 }
276 }
277 },
278 _ => {
279 return Err(SError::custom(pid, &doc, "HTTPError", "fifth parameter for an HTTP request must be a response object (obj)"));
280 }
281 }
282 }
283
284 for header in headers {
286 request = request.set(header.0.as_str(), header.1.as_str());
287 }
288 request = request.timeout(timeout);
289
290 let response_res;
292 if let Some(body) = str_body {
293 response_res = request.send_string(&body);
294 } else if let Some(body) = blob_body {
295 response_res = request.send_bytes(&body);
296 } else {
297 response_res = request.call();
298 }
299 let response;
300 match response_res {
301 Ok(res) => response = res,
302 Err(error) => return Err(SError::custom(pid, &doc, "HTTPError", &format!("error sending request: {}", error.to_string()))),
303 }
304
305 let content_type = response.content_type().to_owned();
307 let mut response_headers = BTreeMap::new();
308 for name in response.headers_names() {
309 if let Some(value) = response.header(&name) {
310 response_headers.insert(SVal::String(name), SVal::String(value.to_owned()));
311 }
312 }
313
314 let mut buf: Vec<u8> = vec![];
316 let res = response.into_reader()
317 .take(((10 * 1_024 * 1_024) + 1) as u64)
318 .read_to_end(&mut buf);
319 if res.is_err() {
320 return Err(SError::custom(pid, &doc, "HTTPError", &format!("error reading response into buffer: {}", res.err().unwrap().to_string())));
321 }
322 if buf.len() > (10 * 1_024 * 1_024) {
323 return Err(SError::custom(pid, &doc, "HTTPError", "response is too large to be read into a buffer"));
324 }
325
326 if let Some(response_obj) = response_obj {
328 let mut bytes = Bytes::from(buf.clone());
329 let as_name = response_obj.path(&doc.graph);
330 doc.header_import(pid, &content_type, &content_type, &mut bytes, &as_name)?;
331 }
332
333 return Ok(SVal::Tuple(vec![SVal::String(content_type), SVal::Map(response_headers), SVal::Blob(buf)]));
335 }
336}
337
338
339#[cfg(test)]
340mod tests {
341 use std::sync::Arc;
342 use stof::SDoc;
343 use crate::HTTPLibrary;
344
345
346 #[test]
347 fn get() {
348 let stof = r#"
349 fn main(): str {
350 let url = 'https://restcountries.com/v3.1/name/germany';
351
352 // Using a response object, we are telling the document to call header_import using the responses 'content-type' as a format,
353 // parsing the response into this object. The object can be created like so, or be an already created obj in the document somewhere.
354 let obj = new {};
355
356 let resp = HTTP.get(url, obj);
357
358 // return resp[2] as str; // This will convert the blob body to a string using utf-8, returning the entire response body
359
360 let first = obj.field[0];
361 return `${first.altSpellings[1]} has an area of ${first.area}`;
362 }
363 "#;
364 let mut doc = SDoc::src(stof, "stof").unwrap();
365 doc.load_lib(Arc::new(HTTPLibrary::default()));
366
367 let res = doc.call_func("main", None, vec![]).unwrap();
368 assert_eq!(res.to_string(), "Federal Republic of Germany has an area of 357114");
369 }
370}