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}