Skip to main content

robinpath_modules/modules/
url_mod.rs

1use robinpath::{RobinPath, Value};
2use url::Url;
3
4pub fn register(rp: &mut RobinPath) {
5    rp.register_builtin("url.parse", |args, _| {
6        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7        match Url::parse(&s) {
8            Ok(parsed) => {
9                let mut obj = indexmap::IndexMap::new();
10                obj.insert(
11                    "protocol".to_string(),
12                    Value::String(parsed.scheme().to_string()),
13                );
14                obj.insert(
15                    "hostname".to_string(),
16                    Value::String(parsed.host_str().unwrap_or("").to_string()),
17                );
18                obj.insert(
19                    "port".to_string(),
20                    match parsed.port() {
21                        Some(p) => Value::Number(p as f64),
22                        None => Value::Null,
23                    },
24                );
25                obj.insert(
26                    "pathname".to_string(),
27                    Value::String(parsed.path().to_string()),
28                );
29                obj.insert(
30                    "search".to_string(),
31                    Value::String(
32                        parsed
33                            .query()
34                            .map(|q| format!("?{}", q))
35                            .unwrap_or_default(),
36                    ),
37                );
38                obj.insert(
39                    "hash".to_string(),
40                    Value::String(
41                        parsed
42                            .fragment()
43                            .map(|f| format!("#{}", f))
44                            .unwrap_or_default(),
45                    ),
46                );
47                Ok(Value::Object(obj))
48            }
49            Err(e) => Err(format!("url parse error: {}", e)),
50        }
51    });
52
53    rp.register_builtin("url.format", |args, _| {
54        let parts = args.first().cloned().unwrap_or(Value::Null);
55        if let Value::Object(obj) = &parts {
56            let protocol = obj
57                .get("protocol")
58                .map(|v| v.to_display_string())
59                .unwrap_or_else(|| "https".to_string());
60            let hostname = obj
61                .get("hostname")
62                .map(|v| v.to_display_string())
63                .unwrap_or_default();
64            let port = obj.get("port").and_then(|v| v.as_number());
65            let pathname = obj
66                .get("pathname")
67                .map(|v| v.to_display_string())
68                .unwrap_or_else(|| "/".to_string());
69            let search = obj
70                .get("search")
71                .map(|v| v.to_display_string())
72                .unwrap_or_default();
73            let hash = obj
74                .get("hash")
75                .map(|v| v.to_display_string())
76                .unwrap_or_default();
77
78            let mut result = format!("{}://{}", protocol, hostname);
79            if let Some(p) = port {
80                let p = p as u16;
81                result.push_str(&format!(":{}", p));
82            }
83            result.push_str(&pathname);
84            result.push_str(&search);
85            result.push_str(&hash);
86            Ok(Value::String(result))
87        } else {
88            Err("url.format expects an object".to_string())
89        }
90    });
91
92    rp.register_builtin("url.getParam", |args, _| {
93        let url_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
94        let param = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
95        match Url::parse(&url_str) {
96            Ok(parsed) => {
97                let value = parsed.query_pairs().find(|(k, _)| k == &param).map(|(_, v)| v.to_string());
98                match value {
99                    Some(v) => Ok(Value::String(v)),
100                    None => Ok(Value::Null),
101                }
102            }
103            Err(_) => Ok(Value::Null),
104        }
105    });
106
107    rp.register_builtin("url.setParam", |args, _| {
108        let url_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
109        let param = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
110        let value = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
111        match Url::parse(&url_str) {
112            Ok(mut parsed) => {
113                // Remove existing param and add new one
114                let pairs: Vec<(String, String)> = parsed
115                    .query_pairs()
116                    .filter(|(k, _)| k != &param)
117                    .map(|(k, v)| (k.to_string(), v.to_string()))
118                    .collect();
119                {
120                    let mut query = parsed.query_pairs_mut();
121                    query.clear();
122                    for (k, v) in &pairs {
123                        query.append_pair(k, v);
124                    }
125                    query.append_pair(&param, &value);
126                }
127                Ok(Value::String(parsed.to_string()))
128            }
129            Err(e) => Err(format!("url parse error: {}", e)),
130        }
131    });
132
133    rp.register_builtin("url.removeParam", |args, _| {
134        let url_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
135        let param = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
136        match Url::parse(&url_str) {
137            Ok(mut parsed) => {
138                let pairs: Vec<(String, String)> = parsed
139                    .query_pairs()
140                    .filter(|(k, _)| k != &param)
141                    .map(|(k, v)| (k.to_string(), v.to_string()))
142                    .collect();
143                {
144                    let mut query = parsed.query_pairs_mut();
145                    query.clear();
146                    for (k, v) in &pairs {
147                        query.append_pair(k, v);
148                    }
149                }
150                // Remove trailing ? if no params left
151                if parsed.query() == Some("") {
152                    parsed.set_query(None);
153                }
154                Ok(Value::String(parsed.to_string()))
155            }
156            Err(e) => Err(format!("url parse error: {}", e)),
157        }
158    });
159
160    rp.register_builtin("url.getParams", |args, _| {
161        let url_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
162        match Url::parse(&url_str) {
163            Ok(parsed) => {
164                let mut obj = indexmap::IndexMap::new();
165                for (k, v) in parsed.query_pairs() {
166                    obj.insert(k.to_string(), Value::String(v.to_string()));
167                }
168                Ok(Value::Object(obj))
169            }
170            Err(_) => Ok(Value::Object(indexmap::IndexMap::new())),
171        }
172    });
173
174    rp.register_builtin("url.isValid", |args, _| {
175        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
176        Ok(Value::Bool(Url::parse(&s).is_ok()))
177    });
178
179    rp.register_builtin("url.encode", |args, _| {
180        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
181        Ok(Value::String(percent_encode(&s)))
182    });
183
184    rp.register_builtin("url.decode", |args, _| {
185        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
186        Ok(Value::String(percent_decode(&s)))
187    });
188}
189
190fn percent_encode(s: &str) -> String {
191    let mut result = String::new();
192    for byte in s.bytes() {
193        match byte {
194            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
195                result.push(byte as char);
196            }
197            _ => {
198                result.push_str(&format!("%{:02X}", byte));
199            }
200        }
201    }
202    result
203}
204
205fn percent_decode(s: &str) -> String {
206    let mut result = Vec::new();
207    let bytes = s.as_bytes();
208    let mut i = 0;
209    while i < bytes.len() {
210        if bytes[i] == b'%' && i + 2 < bytes.len() {
211            if let Ok(byte) = u8::from_str_radix(
212                &String::from_utf8_lossy(&bytes[i + 1..i + 3]),
213                16,
214            ) {
215                result.push(byte);
216                i += 3;
217                continue;
218            }
219        }
220        result.push(bytes[i]);
221        i += 1;
222    }
223    String::from_utf8_lossy(&result).to_string()
224}