1use std::collections::HashMap;
2
3use camel_api::CamelError;
4
5#[derive(Clone, PartialEq)]
9pub struct UriComponents {
10 pub scheme: String,
12 pub path: String,
14 pub params: HashMap<String, String>,
16}
17
18const SENSITIVE_KEYS: &[&str] = &[
19 "password",
20 "secret",
21 "token",
22 "credential",
23 "apikey",
24 "accesskey",
25 "privatekey",
26];
27
28impl std::fmt::Debug for UriComponents {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 let mut redacted_params = std::collections::HashMap::new();
31 for (k, v) in &self.params {
32 if SENSITIVE_KEYS.contains(&k.to_lowercase().as_str()) {
33 redacted_params.insert(k.clone(), "***".to_string());
34 } else {
35 redacted_params.insert(k.clone(), v.clone());
36 }
37 }
38 f.debug_struct("UriComponents")
39 .field("scheme", &self.scheme)
40 .field("path", &self.path)
41 .field("params", &redacted_params)
42 .finish()
43 }
44}
45
46pub fn parse_uri(uri: &str) -> Result<UriComponents, CamelError> {
50 let (scheme, rest) = uri.split_once(':').ok_or_else(|| {
51 CamelError::InvalidUri(format!("missing scheme separator ':' in '{uri}'"))
52 })?;
53
54 if scheme.is_empty() {
55 return Err(CamelError::InvalidUri(format!("empty scheme in '{uri}'")));
56 }
57
58 let (path, params) = match rest.split_once('?') {
59 Some((path, query)) => (path, parse_query(query)),
60 None => (rest, HashMap::new()),
61 };
62
63 Ok(UriComponents {
64 scheme: scheme.to_string(),
65 path: path.to_string(),
66 params,
67 })
68}
69
70fn parse_query(query: &str) -> HashMap<String, String> {
71 query
72 .split('&')
73 .filter(|s| !s.is_empty())
74 .filter_map(|pair| {
75 let (key, value) = pair.split_once('=')?;
76 Some((key.to_string(), value.to_string()))
77 })
78 .collect()
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_parse_simple_uri() {
87 let result = parse_uri("timer:tick").unwrap();
88 assert_eq!(result.scheme, "timer");
89 assert_eq!(result.path, "tick");
90 assert!(result.params.is_empty());
91 }
92
93 #[test]
94 fn test_parse_uri_with_params() {
95 let result = parse_uri("timer:tick?period=1000&delay=500").unwrap();
96 assert_eq!(result.scheme, "timer");
97 assert_eq!(result.path, "tick");
98 assert_eq!(result.params.get("period"), Some(&"1000".to_string()));
99 assert_eq!(result.params.get("delay"), Some(&"500".to_string()));
100 }
101
102 #[test]
103 fn test_parse_uri_with_single_param() {
104 let result = parse_uri("log:info?level=debug").unwrap();
105 assert_eq!(result.scheme, "log");
106 assert_eq!(result.path, "info");
107 assert_eq!(result.params.get("level"), Some(&"debug".to_string()));
108 }
109
110 #[test]
111 fn test_parse_uri_no_scheme() {
112 let result = parse_uri("noscheme");
113 assert!(result.is_err());
114 }
115
116 #[test]
117 fn test_parse_uri_empty_scheme() {
118 let result = parse_uri(":path");
119 assert!(result.is_err());
120 }
121
122 #[test]
123 fn test_parse_direct_uri() {
124 let result = parse_uri("direct:myRoute").unwrap();
125 assert_eq!(result.scheme, "direct");
126 assert_eq!(result.path, "myRoute");
127 assert!(result.params.is_empty());
128 }
129
130 #[test]
131 fn test_parse_mock_uri() {
132 let result = parse_uri("mock:result").unwrap();
133 assert_eq!(result.scheme, "mock");
134 assert_eq!(result.path, "result");
135 }
136
137 #[test]
138 fn test_parse_http_uri_simple() {
139 let result = parse_uri("http://localhost:8080/api/users").unwrap();
140 assert_eq!(result.scheme, "http");
141 assert_eq!(result.path, "//localhost:8080/api/users");
142 assert!(result.params.is_empty());
143 }
144
145 #[test]
146 fn test_parse_https_uri_with_camel_params() {
147 let result = parse_uri(
148 "https://api.example.com/v1/data?httpMethod=POST&throwExceptionOnFailure=false",
149 )
150 .unwrap();
151 assert_eq!(result.scheme, "https");
152 assert_eq!(result.path, "//api.example.com/v1/data");
153 assert_eq!(result.params.get("httpMethod"), Some(&"POST".to_string()));
154 assert_eq!(
155 result.params.get("throwExceptionOnFailure"),
156 Some(&"false".to_string())
157 );
158 }
159
160 #[test]
161 fn test_parse_http_uri_no_path() {
162 let result = parse_uri("http://localhost:8080").unwrap();
163 assert_eq!(result.scheme, "http");
164 assert_eq!(result.path, "//localhost:8080");
165 assert!(result.params.is_empty());
166 }
167
168 #[test]
169 fn test_parse_http_uri_with_port_and_query() {
170 let result = parse_uri("http://example.com:3000/api?connectTimeout=5000").unwrap();
171 assert_eq!(result.scheme, "http");
172 assert_eq!(result.path, "//example.com:3000/api");
173 assert_eq!(
174 result.params.get("connectTimeout"),
175 Some(&"5000".to_string())
176 );
177 }
178
179 #[test]
180 fn test_uri_components_debug_redacts_sensitive_params() {
181 let uri = parse_uri("timer:tick?password=secret&token=abc123&name=hello").unwrap();
182 let debug_output = format!("{:?}", uri);
183 assert!(
184 !debug_output.contains("secret"),
185 "Debug must not contain password value"
186 );
187 assert!(
188 !debug_output.contains("abc123"),
189 "Debug must not contain token value"
190 );
191 assert!(
192 debug_output.contains("hello"),
193 "Debug should contain non-sensitive param values"
194 );
195 assert!(
196 debug_output.contains("password"),
197 "Debug should show param key 'password'"
198 );
199 }
200
201 #[test]
202 fn test_uri_components_debug_redacts_case_insensitive() {
203 let uri = parse_uri("timer:tick?Password=secret&TOKEN=abc123").unwrap();
204 let debug_output = format!("{:?}", uri);
205 assert!(
206 !debug_output.contains("secret"),
207 "Debug must redact 'Password' (capitalized)"
208 );
209 assert!(
210 !debug_output.contains("abc123"),
211 "Debug must redact 'TOKEN' (uppercase)"
212 );
213 }
214}