robinpath_modules/modules/
api_mod.rs1use 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 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 req = apply_profile_headers(req, url, state);
187
188 req = apply_options(req, options, state);
190
191 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 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 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 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}