1use std::{collections::BTreeMap, io::Read, ops::Deref, time::Duration};
18use bytes::Bytes;
19use stof::{lang::SError, Library, SDoc, SNodeRef, SNum, 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> {
76 let mut method = name.to_string();
78 match name {
79 "send" => {
80 if parameters.len() > 0 {
81 let name = parameters.remove(0);
82 method = name.owned_to_string();
83 }
84 },
85 "ok" => {
86 if parameters.len() > 0 {
87 let code = parameters.pop().unwrap();
88 match code {
89 SVal::Number(num) => {
90 let code = num.int();
91 return Ok(SVal::Bool(code >= 200 && code < 300));
92 },
93 SVal::Boxed(val) => {
94 let val = val.lock().unwrap();
95 let val = val.deref();
96 match val {
97 SVal::Number(num) => {
98 let code = num.int();
99 return Ok(SVal::Bool(code >= 200 && code < 300));
100 },
101 _ => {
102 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
103 }
104 }
105 },
106 _ => {
107 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
108 }
109 }
110 }
111 },
112 "clientError" => {
113 if parameters.len() > 0 {
114 let code = parameters.pop().unwrap();
115 match code {
116 SVal::Number(num) => {
117 let code = num.int();
118 return Ok(SVal::Bool(code >= 400 && code < 500));
119 },
120 SVal::Boxed(val) => {
121 let val = val.lock().unwrap();
122 let val = val.deref();
123 match val {
124 SVal::Number(num) => {
125 let code = num.int();
126 return Ok(SVal::Bool(code >= 400 && code < 500));
127 },
128 _ => {
129 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
130 }
131 }
132 },
133 _ => {
134 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
135 }
136 }
137 }
138 },
139 "serverError" => {
140 if parameters.len() > 0 {
141 let code = parameters.pop().unwrap();
142 match code {
143 SVal::Number(num) => {
144 let code = num.int();
145 return Ok(SVal::Bool(code >= 500 && code < 600));
146 },
147 SVal::Boxed(val) => {
148 let val = val.lock().unwrap();
149 let val = val.deref();
150 match val {
151 SVal::Number(num) => {
152 let code = num.int();
153 return Ok(SVal::Bool(code >= 500 && code < 600));
154 },
155 _ => {
156 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
157 }
158 }
159 },
160 _ => {
161 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non-numerical status code"));
162 }
163 }
164 }
165 },
166 "contentType" => {
167 if parameters.len() > 0 {
168 let headers = parameters.pop().unwrap();
169 match headers {
170 SVal::Map(map) => {
171 if let Some(ctype) = map.get(&"Content-Type".into()) {
172 return Ok(ctype.clone());
173 } else if let Some(ctype) = map.get(&"content-type".into()) {
174 return Ok(ctype.clone());
175 }
176 return Ok(SVal::Null); },
178 SVal::Boxed(val) => {
179 let val = val.lock().unwrap();
180 let val = val.deref();
181 match val {
182 SVal::Map(map) => {
183 if let Some(ctype) = map.get(&"Content-Type".into()) {
184 return Ok(ctype.clone());
185 } else if let Some(ctype) = map.get(&"content-type".into()) {
186 return Ok(ctype.clone());
187 }
188 return Ok(SVal::Null); },
190 _ => {}
191 }
192 },
193 _ => {}
194 }
195 return Err(SError::custom(pid, &doc, "HTTPError", "cannot check a non map set of headers"));
196 }
197 },
198 _ => {}
199 }
200
201 let url;
202 if parameters.len() > 0 {
203 match ¶meters[0] {
204 SVal::String(val) => {
205 url = val.clone();
206 },
207 SVal::Boxed(val) => {
208 let val = val.lock().unwrap();
209 let val = val.deref();
210 match val {
211 SVal::String(val) => {
212 url = val.clone();
213 },
214 _ => {
215 return Err(SError::custom(pid, &doc, "HTTPError", "url must be a string"));
216 }
217 }
218 },
219 _ => {
220 return Err(SError::custom(pid, &doc, "HTTPError", "url must be a string"));
221 }
222 }
223 } else {
224 return Err(SError::custom(pid, &doc, "HTTPError", "must provide a URL as the first parameter when calling into the HTTP library"));
225 }
226
227 let mut request;
228 match method.to_lowercase().as_str() {
229 "get" => request = self.agent.get(&url),
230 "head" => request = self.agent.head(&url),
231 "patch" => request = self.agent.patch(&url),
232 "post" => request = self.agent.post(&url),
233 "put" => request = self.agent.put(&url),
234 "delete" => request = self.agent.delete(&url),
235 _ => {
236 return Err(SError::custom(pid, &doc, "HTTPError", &format!("unrecognized HTTP library function: {}", method)));
237 }
238 }
239
240 let mut headers = Vec::new();
241 let mut str_body: Option<String> = None;
242 let mut blob_body: Option<Vec<u8>> = None;
243 let mut timeout = Duration::from_secs(5);
244 let mut response_obj: Option<SNodeRef> = None;
245 if parameters.len() > 1 {
246 match ¶meters[1] {
247 SVal::Array(vals) => {
248 for val in vals {
249 match val {
250 SVal::Tuple(vals) => {
251 if vals.len() == 2 {
252 headers.push((vals[0].to_string(), vals[1].to_string()));
253 }
254 },
255 _ => {}
256 }
257 }
258 },
259 SVal::Map(map) => {
260 for (k, v) in map {
261 headers.push((k.to_string(), v.to_string()));
262 }
263 },
264 SVal::String(body) => {
265 str_body = Some(body.clone());
266 },
267 SVal::Blob(body) => {
268 blob_body = Some(body.clone());
269 },
270 SVal::Number(num) => {
271 let seconds = num.float_with_units(SUnits::Seconds);
272 timeout = Duration::from_secs(seconds as u64);
273 },
274 SVal::Object(nref) => {
275 response_obj = Some(nref.clone());
276 },
277 SVal::Boxed(val) => {
278 let val = val.lock().unwrap();
279 let val = val.deref();
280 match val {
281 SVal::Array(vals) => {
282 for val in vals {
283 match val {
284 SVal::Tuple(vals) => {
285 if vals.len() == 2 {
286 headers.push((vals[0].to_string(), vals[1].to_string()));
287 }
288 },
289 _ => {}
290 }
291 }
292 },
293 SVal::Map(map) => {
294 for (k, v) in map {
295 headers.push((k.to_string(), v.to_string()));
296 }
297 },
298 SVal::String(body) => {
299 str_body = Some(body.clone());
300 },
301 SVal::Blob(body) => {
302 blob_body = Some(body.clone());
303 },
304 SVal::Number(num) => {
305 let seconds = num.float_with_units(SUnits::Seconds);
306 timeout = Duration::from_secs(seconds as u64);
307 },
308 SVal::Object(nref) => {
309 response_obj = Some(nref.clone());
310 },
311 _ => {
312 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)"));
313 }
314 }
315 },
316 _ => {
317 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)"));
318 }
319 }
320 }
321 if parameters.len() > 2 {
322 match ¶meters[2] {
323 SVal::String(body) => {
324 str_body = Some(body.clone());
325 },
326 SVal::Blob(body) => {
327 blob_body = Some(body.clone());
328 },
329 SVal::Number(num) => {
330 let seconds = num.float_with_units(SUnits::Seconds);
331 timeout = Duration::from_secs(seconds as u64);
332 },
333 SVal::Object(nref) => {
334 response_obj = Some(nref.clone());
335 },
336 SVal::Boxed(val) => {
337 let val = val.lock().unwrap();
338 let val = val.deref();
339 match val {
340 SVal::String(body) => {
341 str_body = Some(body.clone());
342 },
343 SVal::Blob(body) => {
344 blob_body = Some(body.clone());
345 },
346 SVal::Number(num) => {
347 let seconds = num.float_with_units(SUnits::Seconds);
348 timeout = Duration::from_secs(seconds as u64);
349 },
350 SVal::Object(nref) => {
351 response_obj = Some(nref.clone());
352 },
353 _ => {
354 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)"));
355 }
356 }
357 },
358 _ => {
359 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)"));
360 }
361 }
362 }
363 if parameters.len() > 3 {
364 match ¶meters[3] {
365 SVal::Number(num) => {
366 let seconds = num.float_with_units(SUnits::Seconds);
367 timeout = Duration::from_secs(seconds as u64);
368 },
369 SVal::Object(nref) => {
370 response_obj = Some(nref.clone());
371 },
372 SVal::Boxed(val) => {
373 let val = val.lock().unwrap();
374 let val = val.deref();
375 match val {
376 SVal::Number(num) => {
377 let seconds = num.float_with_units(SUnits::Seconds);
378 timeout = Duration::from_secs(seconds as u64);
379 },
380 SVal::Object(nref) => {
381 response_obj = Some(nref.clone());
382 },
383 _ => {
384 return Err(SError::custom(pid, &doc, "HTTPError", "fourth parameter for an HTTP request must be a timeout (float | units) or a response object (obj)"));
385 }
386 }
387 },
388 _ => {
389 return Err(SError::custom(pid, &doc, "HTTPError", "fourth parameter for an HTTP request must be a timeout (float | units) or a response object (obj)"));
390 }
391 }
392 }
393 if parameters.len() > 4 {
394 match ¶meters[4] {
395 SVal::Object(nref) => {
396 response_obj = Some(nref.clone());
397 },
398 SVal::Boxed(val) => {
399 let val = val.lock().unwrap();
400 let val = val.deref();
401 match val {
402 SVal::Object(nref) => {
403 response_obj = Some(nref.clone());
404 },
405 _ => {
406 return Err(SError::custom(pid, &doc, "HTTPError", "fifth parameter for an HTTP request must be a response object (obj)"));
407 }
408 }
409 },
410 _ => {
411 return Err(SError::custom(pid, &doc, "HTTPError", "fifth parameter for an HTTP request must be a response object (obj)"));
412 }
413 }
414 }
415
416 for header in headers {
418 request = request.set(header.0.as_str(), header.1.as_str());
419 }
420 request = request.timeout(timeout);
421
422 let response_res;
424 if let Some(body) = str_body {
425 response_res = request.send_string(&body);
426 } else if let Some(body) = blob_body {
427 response_res = request.send_bytes(&body);
428 } else {
429 response_res = request.call();
430 }
431 let response;
432 match response_res {
433 Ok(res) => response = res,
434 Err(error) => return Err(SError::custom(pid, &doc, "HTTPError", &format!("error sending request: {}", error.to_string()))),
435 }
436
437 let status = response.status();
439
440 let content_type = response.content_type().to_owned();
442 let mut response_headers = BTreeMap::new();
443 for name in response.headers_names() {
444 if let Some(value) = response.header(&name) {
445 response_headers.insert(SVal::String(name), SVal::String(value.to_owned()));
446 }
447 }
448
449 let mut buf: Vec<u8> = vec![];
451 let res = response.into_reader()
452 .take(((10 * 1_024 * 1_024) + 1) as u64)
453 .read_to_end(&mut buf);
454 if res.is_err() {
455 return Err(SError::custom(pid, &doc, "HTTPError", &format!("error reading response into buffer: {}", res.err().unwrap().to_string())));
456 }
457 if buf.len() > (10 * 1_024 * 1_024) {
458 return Err(SError::custom(pid, &doc, "HTTPError", "response is too large to be read into a buffer"));
459 }
460
461 if let Some(response_obj) = response_obj {
463 let mut bytes = Bytes::from(buf.clone());
464 let as_name = response_obj.path(&doc.graph);
465 doc.header_import(pid, &content_type, &content_type, &mut bytes, &as_name)?;
466 }
467
468 return Ok(SVal::Tuple(vec![SVal::Number(SNum::I64(status as i64)), SVal::Map(response_headers), SVal::Blob(buf)]));
470 }
471}
472
473
474#[cfg(test)]
475mod tests {
476 use std::sync::Arc;
477 use stof::SDoc;
478 use crate::HTTPLibrary;
479
480
481 #[test]
482 fn get() {
483 let stof = r#"
484 fn main(): str {
485 let url = 'https://restcountries.com/v3.1/name/germany';
486
487 // Using a response object, we are telling the document to call header_import using the responses 'content-type' as a format,
488 // parsing the response into this object. The object can be created like so, or be an already created obj in the document somewhere.
489 let obj = new {};
490
491 let resp = HTTP.get(url, obj);
492
493 // return resp[2] as str; // This will convert the blob body to a string using utf-8, returning the entire response body
494
495 let first = obj.field[0];
496 return `${first.altSpellings[1]} has an area of ${first.area}`;
497 }
498 "#;
499 let mut doc = SDoc::src(stof, "stof").unwrap();
500 doc.load_lib(Arc::new(HTTPLibrary::default()));
501
502 let res = doc.call_func("main", None, vec![]).unwrap();
503 assert_eq!(res.to_string(), "Federal Republic of Germany has an area of 357114");
504 }
505}