Skip to main content

jpx_core/extensions/
url_fns.rs

1//! URL parsing and manipulation functions.
2
3use std::collections::HashSet;
4
5use serde_json::Value;
6
7use crate::functions::Function;
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12/// Register URL functions with the runtime, filtered by the enabled set.
13pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
14    register_if_enabled(runtime, "url_encode", enabled, Box::new(UrlEncodeFn::new()));
15    register_if_enabled(runtime, "url_decode", enabled, Box::new(UrlDecodeFn::new()));
16    register_if_enabled(runtime, "url_parse", enabled, Box::new(UrlParseFn::new()));
17}
18
19// =============================================================================
20// url_encode(string) -> string
21// =============================================================================
22
23defn!(UrlEncodeFn, vec![arg!(string)], None);
24
25impl Function for UrlEncodeFn {
26    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
27        self.signature.validate(args, ctx)?;
28
29        let input = args[0].as_str().ok_or_else(|| {
30            crate::JmespathError::from_ctx(
31                ctx,
32                crate::ErrorReason::Parse("Expected string argument".to_owned()),
33            )
34        })?;
35
36        let encoded = urlencoding::encode(input);
37        Ok(Value::String(encoded.into_owned()))
38    }
39}
40
41// =============================================================================
42// url_decode(string) -> string
43// =============================================================================
44
45defn!(UrlDecodeFn, vec![arg!(string)], None);
46
47impl Function for UrlDecodeFn {
48    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
49        self.signature.validate(args, ctx)?;
50
51        let input = args[0].as_str().ok_or_else(|| {
52            crate::JmespathError::from_ctx(
53                ctx,
54                crate::ErrorReason::Parse("Expected string argument".to_owned()),
55            )
56        })?;
57
58        match urlencoding::decode(input) {
59            Ok(decoded) => Ok(Value::String(decoded.into_owned())),
60            Err(_) => Err(crate::JmespathError::from_ctx(
61                ctx,
62                crate::ErrorReason::Parse("Invalid URL-encoded input".to_owned()),
63            )),
64        }
65    }
66}
67
68// =============================================================================
69// url_parse(string) -> object (parse URL into components)
70// =============================================================================
71
72defn!(UrlParseFn, vec![arg!(string)], None);
73
74impl Function for UrlParseFn {
75    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
76        self.signature.validate(args, ctx)?;
77
78        let input = args[0].as_str().ok_or_else(|| {
79            crate::JmespathError::from_ctx(
80                ctx,
81                crate::ErrorReason::Parse("Expected string argument".to_owned()),
82            )
83        })?;
84
85        match url::Url::parse(input) {
86            Ok(parsed) => {
87                let mut result = serde_json::Map::new();
88
89                result.insert(
90                    "scheme".to_string(),
91                    Value::String(parsed.scheme().to_string()),
92                );
93
94                if let Some(host) = parsed.host_str() {
95                    result.insert("host".to_string(), Value::String(host.to_string()));
96                } else {
97                    result.insert("host".to_string(), Value::Null);
98                }
99
100                if let Some(port) = parsed.port() {
101                    result.insert(
102                        "port".to_string(),
103                        Value::Number(serde_json::Number::from(port)),
104                    );
105                } else {
106                    result.insert("port".to_string(), Value::Null);
107                }
108
109                result.insert("path".to_string(), Value::String(parsed.path().to_string()));
110
111                if let Some(query) = parsed.query() {
112                    result.insert("query".to_string(), Value::String(query.to_string()));
113                } else {
114                    result.insert("query".to_string(), Value::Null);
115                }
116
117                if let Some(fragment) = parsed.fragment() {
118                    result.insert("fragment".to_string(), Value::String(fragment.to_string()));
119                } else {
120                    result.insert("fragment".to_string(), Value::Null);
121                }
122
123                if !parsed.username().is_empty() {
124                    result.insert(
125                        "username".to_string(),
126                        Value::String(parsed.username().to_string()),
127                    );
128                }
129
130                if let Some(password) = parsed.password() {
131                    result.insert("password".to_string(), Value::String(password.to_string()));
132                }
133
134                // Add origin field (scheme + host + port)
135                let origin = parsed.origin().ascii_serialization();
136                result.insert("origin".to_string(), Value::String(origin));
137
138                Ok(Value::Object(result))
139            }
140            // Return null for invalid URLs instead of an error
141            Err(_) => Ok(Value::Null),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use crate::Runtime;
149    use serde_json::json;
150
151    fn setup_runtime() -> Runtime {
152        Runtime::builder()
153            .with_standard()
154            .with_all_extensions()
155            .build()
156    }
157
158    #[test]
159    fn test_url_encode() {
160        let runtime = setup_runtime();
161        let expr = runtime.compile("url_encode(@)").unwrap();
162        let data = json!("hello world");
163        let result = expr.search(&data).unwrap();
164        assert_eq!(result.as_str().unwrap(), "hello%20world");
165    }
166
167    #[test]
168    fn test_url_decode() {
169        let runtime = setup_runtime();
170        let expr = runtime.compile("url_decode(@)").unwrap();
171        let data = json!("hello%20world");
172        let result = expr.search(&data).unwrap();
173        assert_eq!(result.as_str().unwrap(), "hello world");
174    }
175
176    #[test]
177    fn test_url_parse() {
178        let runtime = setup_runtime();
179        let expr = runtime.compile("url_parse(@)").unwrap();
180        let data = json!("https://example.com:8080/path?query=1#frag");
181        let result = expr.search(&data).unwrap();
182        let obj = result.as_object().unwrap();
183        assert_eq!(obj.get("scheme").unwrap().as_str().unwrap(), "https");
184        assert_eq!(obj.get("host").unwrap().as_str().unwrap(), "example.com");
185        assert_eq!(obj.get("port").unwrap().as_f64().unwrap() as u16, 8080);
186    }
187
188    #[test]
189    fn test_url_parse_origin() {
190        let runtime = setup_runtime();
191        let expr = runtime.compile("url_parse(@)").unwrap();
192        let data = json!("https://example.com:8080/path");
193        let result = expr.search(&data).unwrap();
194        let obj = result.as_object().unwrap();
195        assert_eq!(
196            obj.get("origin").unwrap().as_str().unwrap(),
197            "https://example.com:8080"
198        );
199    }
200
201    #[test]
202    fn test_url_parse_invalid_returns_null() {
203        let runtime = setup_runtime();
204        let expr = runtime.compile("url_parse(@)").unwrap();
205        let data = json!("not a valid url");
206        let result = expr.search(&data).unwrap();
207        assert!(result.is_null());
208    }
209}