robinpath_modules/modules/
http_mod.rs1use robinpath::{RobinPath, Value};
2
3pub 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) = ¶ms {
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}