stof_http/
lib.rs

1//
2// Copyright 2024 Formata, Inc. All rights reserved.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//    http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17use 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    /// Scope of this library.
40    /// This is how this library is invoked from Stof.
41    /// Ex. `HTTP.get('https://example.com')`
42    fn scope(&self) -> String {
43        "HTTP".to_string()
44    }
45
46    /// Call an HTTP method in this library.
47    /// Returns (status code: int, headers: map, body: blob).
48    ///
49    /// Supported functions:
50    /// - HTTP.ok - check to see if a status code is OK (in range 200-299)
51    /// - HTTP.clientError - check to see if a status code is 400-499
52    /// - HTTP.serverError - check to see if a status code is 500-599
53    /// - HTTP.contentType - get content type from a header map
54    ///
55    /// - HTTP.send - must take an additional parameter at first (one of below methods).
56    /// - HTTP.get
57    /// - HTTP.head
58    /// - HTTP.patch
59    /// - HTTP.post
60    /// - HTTP.put
61    /// - HTTP.delete
62    ///
63    /// Parameters (in order) for each call:
64    /// - url: str                       - The HTTP request path (REQUIRED)
65    /// - headers: vec[(str, str)] | map - The request headers (OPTIONAL)
66    /// - body: str | blob               - The request body (OPTIONAL)
67    /// - timeout: float | units         - The overall timeout for the request (OPTIONAL) (default 5 seconds - use time units as needed)
68    /// - response_obj: obj              - A response object to parse the response into via doc.header_import with the content type (OPTIONAL)
69    ///
70    /// Basic GET request: `HTTP.get('https://example.com')`
71    ///
72    /// POST request with a body: `HTTP.post('https://example.com', 'this is a string body to send')`
73    ///
74    /// POST request json body and a timeout: `HTTP.post('https://example.com', map(('content-type', 'application/json')), stringify(self, 'json'), 10s)`
75    fn call(&self, pid: &str, doc: &mut SDoc, name: &str, parameters: &mut Vec<SVal>) -> Result<SVal, SError> {
76        // Helper functions first
77        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); // no content type header found
177                        },
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); // no content type header found
189                                },
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 &parameters[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 &parameters[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 &parameters[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 &parameters[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 &parameters[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        // Set headers and timeout
417        for header in headers {
418            request = request.set(header.0.as_str(), header.1.as_str());
419        }
420        request = request.timeout(timeout);
421        
422        // Send with body or call without
423        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        // Get the status of the response
438        let status = response.status();
439
440        // Get content type and headers from the response
441        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        // Read response body into a blob
450        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        // Import the response into a response object if one was provided
462        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 the response status, headers, and body
469        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}