Skip to main content

robinpath_modules/modules/
api_mod.rs

1use 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    // Check if url is a profile reference like "myProfile/path"
164    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    // Apply profile headers if url matches a profile
176    req = apply_profile_headers(req, url);
177
178    // Apply options (headers, timeout, etc.)
179    req = apply_options(req, options);
180
181    // Apply body
182    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    // Check if url starts with a profile ID
207    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        // Profile reference in options
258        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    // Try to parse as JSON
282    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}