robinpath_modules/modules/
url_mod.rs1use 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 == ¶m).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 let pairs: Vec<(String, String)> = parsed
115 .query_pairs()
116 .filter(|(k, _)| k != ¶m)
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(¶m, &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 != ¶m)
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 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}