Skip to main content

robinpath_modules/modules/
http_mod.rs

1use robinpath::{RobinPath, Value};
2
3/// HTTP utilities — cookie handling, sessions, redirects, request building
4pub fn register(rp: &mut RobinPath) {
5    rp.register_builtin("http.request", |args, _| {
6        let method = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "GET".to_string());
7        let url = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
8        let options = args.get(2).cloned().unwrap_or(Value::Null);
9        do_request(&method.to_uppercase(), &url, &options)
10    });
11
12    rp.register_builtin("http.get", |args, _| {
13        let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
14        let options = args.get(1).cloned().unwrap_or(Value::Null);
15        do_request("GET", &url, &options)
16    });
17
18    rp.register_builtin("http.post", |args, _| {
19        let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
20        let options = args.get(1).cloned().unwrap_or(Value::Null);
21        do_request("POST", &url, &options)
22    });
23
24    rp.register_builtin("http.put", |args, _| {
25        let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
26        let options = args.get(1).cloned().unwrap_or(Value::Null);
27        do_request("PUT", &url, &options)
28    });
29
30    rp.register_builtin("http.delete", |args, _| {
31        let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
32        let options = args.get(1).cloned().unwrap_or(Value::Null);
33        do_request("DELETE", &url, &options)
34    });
35
36    rp.register_builtin("http.head", |args, _| {
37        let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
38        let client = reqwest::blocking::Client::new();
39        match client.head(&url).send() {
40            Ok(resp) => {
41                let mut obj = indexmap::IndexMap::new();
42                obj.insert("status".to_string(), Value::Number(resp.status().as_u16() as f64));
43                let mut headers = indexmap::IndexMap::new();
44                for (k, v) in resp.headers() {
45                    if let Ok(val) = v.to_str() {
46                        headers.insert(k.to_string(), Value::String(val.to_string()));
47                    }
48                }
49                obj.insert("headers".to_string(), Value::Object(headers));
50                Ok(Value::Object(obj))
51            }
52            Err(e) => Err(format!("http.head error: {}", e)),
53        }
54    });
55
56    rp.register_builtin("http.buildUrl", |args, _| {
57        let base = args.first().map(|v| v.to_display_string()).unwrap_or_default();
58        let params = args.get(1).cloned().unwrap_or(Value::Null);
59        if let Value::Object(obj) = &params {
60            let pairs: Vec<String> = obj.iter()
61                .map(|(k, v)| format!("{}={}", url_encode(k), url_encode(&v.to_display_string())))
62                .collect();
63            if pairs.is_empty() {
64                Ok(Value::String(base))
65            } else {
66                let sep = if base.contains('?') { "&" } else { "?" };
67                Ok(Value::String(format!("{}{}{}", base, sep, pairs.join("&"))))
68            }
69        } else {
70            Ok(Value::String(base))
71        }
72    });
73
74    rp.register_builtin("http.parseCookies", |args, _| {
75        let header = args.first().map(|v| v.to_display_string()).unwrap_or_default();
76        let mut obj = indexmap::IndexMap::new();
77        for pair in header.split(';') {
78            let trimmed = pair.trim();
79            if let Some(eq) = trimmed.find('=') {
80                let key = trimmed[..eq].trim().to_string();
81                let val = trimmed[eq + 1..].trim().to_string();
82                obj.insert(key, Value::String(val));
83            }
84        }
85        Ok(Value::Object(obj))
86    });
87
88    rp.register_builtin("http.buildCookie", |args, _| {
89        let name = args.first().map(|v| v.to_display_string()).unwrap_or_default();
90        let value = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
91        let options = args.get(2).cloned().unwrap_or(Value::Null);
92        let mut cookie = format!("{}={}", name, value);
93        if let Value::Object(obj) = &options {
94            if let Some(v) = obj.get("path") { cookie.push_str(&format!("; Path={}", v.to_display_string())); }
95            if let Some(v) = obj.get("domain") { cookie.push_str(&format!("; Domain={}", v.to_display_string())); }
96            if let Some(v) = obj.get("maxAge") { cookie.push_str(&format!("; Max-Age={}", v.to_display_string())); }
97            if let Some(Value::Bool(true)) = obj.get("secure") { cookie.push_str("; Secure"); }
98            if let Some(Value::Bool(true)) = obj.get("httpOnly") { cookie.push_str("; HttpOnly"); }
99            if let Some(v) = obj.get("sameSite") { cookie.push_str(&format!("; SameSite={}", v.to_display_string())); }
100        }
101        Ok(Value::String(cookie))
102    });
103
104    rp.register_builtin("http.isSuccess", |args, _| {
105        let status = args.first().map(|v| v.to_number() as u16).unwrap_or(0);
106        Ok(Value::Bool((200..300).contains(&status)))
107    });
108
109    rp.register_builtin("http.isRedirect", |args, _| {
110        let status = args.first().map(|v| v.to_number() as u16).unwrap_or(0);
111        Ok(Value::Bool((300..400).contains(&status)))
112    });
113
114    rp.register_builtin("http.isClientError", |args, _| {
115        let status = args.first().map(|v| v.to_number() as u16).unwrap_or(0);
116        Ok(Value::Bool((400..500).contains(&status)))
117    });
118
119    rp.register_builtin("http.isServerError", |args, _| {
120        let status = args.first().map(|v| v.to_number() as u16).unwrap_or(0);
121        Ok(Value::Bool((500..600).contains(&status)))
122    });
123
124    rp.register_builtin("http.statusText", |args, _| {
125        let status = args.first().map(|v| v.to_number() as u16).unwrap_or(0);
126        let text = match status {
127            200 => "OK", 201 => "Created", 204 => "No Content",
128            301 => "Moved Permanently", 302 => "Found", 304 => "Not Modified",
129            400 => "Bad Request", 401 => "Unauthorized", 403 => "Forbidden",
130            404 => "Not Found", 405 => "Method Not Allowed", 408 => "Request Timeout",
131            409 => "Conflict", 422 => "Unprocessable Entity", 429 => "Too Many Requests",
132            500 => "Internal Server Error", 502 => "Bad Gateway",
133            503 => "Service Unavailable", 504 => "Gateway Timeout",
134            _ => "Unknown",
135        };
136        Ok(Value::String(text.to_string()))
137    });
138}
139
140fn do_request(method: &str, url: &str, options: &Value) -> Result<Value, String> {
141    let client = reqwest::blocking::Client::builder()
142        .redirect(reqwest::redirect::Policy::limited(10))
143        .build()
144        .map_err(|e| format!("http client error: {}", e))?;
145
146    let mut req = match method {
147        "GET" => client.get(url),
148        "POST" => client.post(url),
149        "PUT" => client.put(url),
150        "PATCH" => client.patch(url),
151        "DELETE" => client.delete(url),
152        _ => return Err(format!("unsupported method: {}", method)),
153    };
154
155    if let Value::Object(opts) = options {
156        if let Some(Value::Object(headers)) = opts.get("headers") {
157            for (k, v) in headers {
158                req = req.header(k.as_str(), v.to_display_string());
159            }
160        }
161        if let Some(body) = opts.get("body") {
162            match body {
163                Value::Object(_) | Value::Array(_) => {
164                    req = req.header("Content-Type", "application/json").body(body.to_json_string());
165                }
166                _ => {
167                    req = req.body(body.to_display_string());
168                }
169            }
170        }
171        if let Some(timeout) = opts.get("timeout") {
172            let ms = timeout.to_number() as u64;
173            req = req.timeout(std::time::Duration::from_millis(ms));
174        }
175    }
176
177    match req.send() {
178        Ok(resp) => {
179            let status = resp.status().as_u16();
180            let mut headers = indexmap::IndexMap::new();
181            for (k, v) in resp.headers() {
182                if let Ok(val) = v.to_str() {
183                    headers.insert(k.to_string(), Value::String(val.to_string()));
184                }
185            }
186            let body_text = resp.text().map_err(|e| format!("body error: {}", e))?;
187            let body = match serde_json::from_str::<serde_json::Value>(&body_text) {
188                Ok(json) => Value::from(json),
189                Err(_) => Value::String(body_text),
190            };
191            let mut obj = indexmap::IndexMap::new();
192            obj.insert("status".to_string(), Value::Number(status as f64));
193            obj.insert("headers".to_string(), Value::Object(headers));
194            obj.insert("body".to_string(), body);
195            Ok(Value::Object(obj))
196        }
197        Err(e) => Err(format!("http error: {}", e)),
198    }
199}
200
201fn url_encode(s: &str) -> String {
202    let mut result = String::new();
203    for b in s.bytes() {
204        match b {
205            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
206                result.push(b as char);
207            }
208            _ => result.push_str(&format!("%{:02X}", b)),
209        }
210    }
211    result
212}