jpx_core/extensions/
url_fns.rs1use 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
12pub 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
19defn!(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
41defn!(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
68defn!(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 let origin = parsed.origin().ascii_serialization();
136 result.insert("origin".to_string(), Value::String(origin));
137
138 Ok(Value::Object(result))
139 }
140 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}