Skip to main content

robinpath_modules/modules/
api_mod.rs

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