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, 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    ///
48    /// Supported functions:
49    /// - HTTP.get
50    /// - HTTP.head
51    /// - HTTP.patch
52    /// - HTTP.post
53    /// - HTTP.put
54    /// - HTTP.delete
55    ///
56    /// Parameters (in order) for each call:
57    /// - url: str                       - The HTTP request path (REQUIRED)
58    /// - headers: vec[(str, str)] | map - The request headers (OPTIONAL)
59    /// - body: str | blob               - The request body (OPTIONAL)
60    /// - timeout: float | units         - The overall timeout for the request (OPTIONAL) (default 5 seconds - use time units as needed)
61    /// - response_obj: obj              - A response object to parse the response into via doc.header_import with the content type (OPTIONAL)
62    ///
63    /// Basic GET request: `HTTP.get('https://example.com')`
64    ///
65    /// POST request with a body: `HTTP.post('https://example.com', 'this is a string body to send')`
66    ///
67    /// POST request json body and a timeout: `HTTP.post('https://example.com', map(('content-type', 'application/json')), stringify(self, 'json'), 10s)`
68    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 &parameters[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 &parameters[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 &parameters[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 &parameters[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 &parameters[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        // Set headers and timeout
285        for header in headers {
286            request = request.set(header.0.as_str(), header.1.as_str());
287        }
288        request = request.timeout(timeout);
289        
290        // Send with body or call without
291        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        // Get content type and headers from the response
306        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        // Read response body into a blob
315        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        // Import the response into a response object if one was provided
327        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 the response content type, headers, and body
334        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}