robinpath_modules/modules/
api_mod.rs1use robinpath::{RobinPath, Value};
2use std::collections::HashMap;
3use std::sync::Mutex;
4
5struct ApiProfile {
6 base_url: String,
7 headers: HashMap<String, String>,
8}
9
10static PROFILES: std::sync::LazyLock<Mutex<HashMap<String, ApiProfile>>> =
11 std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
12
13pub fn register(rp: &mut RobinPath) {
14 rp.register_builtin("api.get", |args, _| {
15 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
16 let options = args.get(1).cloned().unwrap_or(Value::Null);
17 http_request("GET", &url, None, &options)
18 });
19
20 rp.register_builtin("api.post", |args, _| {
21 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
22 let body = args.get(1).cloned();
23 let options = args.get(2).cloned().unwrap_or(Value::Null);
24 http_request("POST", &url, body.as_ref(), &options)
25 });
26
27 rp.register_builtin("api.put", |args, _| {
28 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
29 let body = args.get(1).cloned();
30 let options = args.get(2).cloned().unwrap_or(Value::Null);
31 http_request("PUT", &url, body.as_ref(), &options)
32 });
33
34 rp.register_builtin("api.patch", |args, _| {
35 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36 let body = args.get(1).cloned();
37 let options = args.get(2).cloned().unwrap_or(Value::Null);
38 http_request("PATCH", &url, body.as_ref(), &options)
39 });
40
41 rp.register_builtin("api.delete", |args, _| {
42 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
43 let options = args.get(1).cloned().unwrap_or(Value::Null);
44 http_request("DELETE", &url, None, &options)
45 });
46
47 rp.register_builtin("api.createProfile", |args, _| {
48 let id = args.first().map(|v| v.to_display_string()).unwrap_or_default();
49 let options = args.get(1).cloned().unwrap_or(Value::Null);
50 let mut base_url = String::new();
51 let mut headers = HashMap::new();
52
53 if let Value::Object(obj) = &options {
54 if let Some(Value::String(url)) = obj.get("baseUrl") {
55 base_url = url.clone();
56 }
57 if let Some(Value::Object(hdrs)) = obj.get("headers") {
58 for (k, v) in hdrs {
59 headers.insert(k.clone(), v.to_display_string());
60 }
61 }
62 }
63
64 let mut profiles = PROFILES.lock().unwrap();
65 profiles.insert(id, ApiProfile { base_url, headers });
66 Ok(Value::Bool(true))
67 });
68
69 rp.register_builtin("api.setAuth", |args, _| {
70 let profile_id = args.first().map(|v| v.to_display_string()).unwrap_or_default();
71 let auth_type = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
72 let token = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
73
74 let mut profiles = PROFILES.lock().unwrap();
75 if let Some(profile) = profiles.get_mut(&profile_id) {
76 let auth_value = match auth_type.to_lowercase().as_str() {
77 "bearer" => format!("Bearer {}", token),
78 "basic" => format!("Basic {}", token),
79 _ => token,
80 };
81 profile
82 .headers
83 .insert("Authorization".to_string(), auth_value);
84 Ok(Value::Bool(true))
85 } else {
86 Err(format!("profile '{}' not found", profile_id))
87 }
88 });
89
90 rp.register_builtin("api.setHeaders", |args, _| {
91 let profile_id = args.first().map(|v| v.to_display_string()).unwrap_or_default();
92 let headers = args.get(1).cloned().unwrap_or(Value::Null);
93
94 let mut profiles = PROFILES.lock().unwrap();
95 if let Some(profile) = profiles.get_mut(&profile_id) {
96 if let Value::Object(hdrs) = &headers {
97 for (k, v) in hdrs {
98 profile.headers.insert(k.clone(), v.to_display_string());
99 }
100 }
101 Ok(Value::Bool(true))
102 } else {
103 Err(format!("profile '{}' not found", profile_id))
104 }
105 });
106
107 rp.register_builtin("api.download", |args, _| {
108 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
109 let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
110 let options = args.get(2).cloned().unwrap_or(Value::Null);
111
112 let client = reqwest::blocking::Client::new();
113 let mut req = client.get(&url);
114 req = apply_options(req, &options);
115
116 match req.send() {
117 Ok(resp) => match resp.bytes() {
118 Ok(bytes) => match std::fs::write(&path, &bytes) {
119 Ok(()) => {
120 let mut obj = indexmap::IndexMap::new();
121 obj.insert("success".to_string(), Value::Bool(true));
122 obj.insert("size".to_string(), Value::Number(bytes.len() as f64));
123 obj.insert("path".to_string(), Value::String(path));
124 Ok(Value::Object(obj))
125 }
126 Err(e) => Err(format!("download write error: {}", e)),
127 },
128 Err(e) => Err(format!("download error: {}", e)),
129 },
130 Err(e) => Err(format!("download error: {}", e)),
131 }
132 });
133
134 rp.register_builtin("api.upload", |args, _| {
135 let url = args.first().map(|v| v.to_display_string()).unwrap_or_default();
136 let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
137 let options = args.get(2).cloned().unwrap_or(Value::Null);
138
139 match std::fs::read(&path) {
140 Ok(data) => {
141 let client = reqwest::blocking::Client::new();
142 let mut req = client.post(&url).body(data);
143 req = apply_options(req, &options);
144
145 match req.send() {
146 Ok(resp) => build_response(resp),
147 Err(e) => Err(format!("upload error: {}", e)),
148 }
149 }
150 Err(e) => Err(format!("upload read error: {}", e)),
151 }
152 });
153}
154
155fn http_request(
156 method: &str,
157 url: &str,
158 body: Option<&Value>,
159 options: &Value,
160) -> Result<Value, String> {
161 let client = reqwest::blocking::Client::new();
162
163 let resolved_url = resolve_url(url);
165
166 let mut req = match method {
167 "GET" => client.get(&resolved_url),
168 "POST" => client.post(&resolved_url),
169 "PUT" => client.put(&resolved_url),
170 "PATCH" => client.patch(&resolved_url),
171 "DELETE" => client.delete(&resolved_url),
172 _ => return Err(format!("unsupported HTTP method: {}", method)),
173 };
174
175 req = apply_profile_headers(req, url);
177
178 req = apply_options(req, options);
180
181 if let Some(body) = body {
183 match body {
184 Value::String(s) => {
185 req = req.body(s.clone());
186 }
187 Value::Object(_) | Value::Array(_) => {
188 let json_str = body.to_json_string();
189 req = req
190 .header("Content-Type", "application/json")
191 .body(json_str);
192 }
193 _ => {
194 req = req.body(body.to_display_string());
195 }
196 }
197 }
198
199 match req.send() {
200 Ok(resp) => build_response(resp),
201 Err(e) => Err(format!("HTTP error: {}", e)),
202 }
203}
204
205fn resolve_url(url: &str) -> String {
206 let profiles = PROFILES.lock().unwrap();
208 for (id, profile) in profiles.iter() {
209 let prefix = format!("{}/", id);
210 if url.starts_with(&prefix) {
211 let path = &url[prefix.len()..];
212 return format!(
213 "{}{}{}",
214 profile.base_url,
215 if profile.base_url.ends_with('/') || path.starts_with('/') {
216 ""
217 } else {
218 "/"
219 },
220 path
221 );
222 }
223 }
224 url.to_string()
225}
226
227fn apply_profile_headers(
228 mut req: reqwest::blocking::RequestBuilder,
229 url: &str,
230) -> reqwest::blocking::RequestBuilder {
231 let profiles = PROFILES.lock().unwrap();
232 for (id, profile) in profiles.iter() {
233 if url.starts_with(&format!("{}/", id)) {
234 for (k, v) in &profile.headers {
235 req = req.header(k.as_str(), v.as_str());
236 }
237 break;
238 }
239 }
240 req
241}
242
243fn apply_options(
244 mut req: reqwest::blocking::RequestBuilder,
245 options: &Value,
246) -> reqwest::blocking::RequestBuilder {
247 if let Value::Object(obj) = options {
248 if let Some(Value::Object(headers)) = obj.get("headers") {
249 for (k, v) in headers {
250 req = req.header(k.as_str(), v.to_display_string());
251 }
252 }
253 if let Some(timeout) = obj.get("timeout") {
254 let ms = timeout.to_number() as u64;
255 req = req.timeout(std::time::Duration::from_millis(ms));
256 }
257 if let Some(Value::String(profile_id)) = obj.get("profile") {
259 let profiles = PROFILES.lock().unwrap();
260 if let Some(profile) = profiles.get(profile_id.as_str()) {
261 for (k, v) in &profile.headers {
262 req = req.header(k.as_str(), v.as_str());
263 }
264 }
265 }
266 }
267 req
268}
269
270fn build_response(resp: reqwest::blocking::Response) -> Result<Value, String> {
271 let status = resp.status().as_u16();
272 let mut headers_map = indexmap::IndexMap::new();
273 for (name, value) in resp.headers() {
274 if let Ok(val) = value.to_str() {
275 headers_map.insert(name.to_string(), Value::String(val.to_string()));
276 }
277 }
278
279 let body_text = resp.text().map_err(|e| format!("body read error: {}", e))?;
280
281 let body = match serde_json::from_str::<serde_json::Value>(&body_text) {
283 Ok(json) => Value::from(json),
284 Err(_) => Value::String(body_text),
285 };
286
287 let mut obj = indexmap::IndexMap::new();
288 obj.insert("status".to_string(), Value::Number(status as f64));
289 obj.insert("headers".to_string(), Value::Object(headers_map));
290 obj.insert("body".to_string(), body);
291 Ok(Value::Object(obj))
292}