Skip to main content

camel_endpoint/
uri.rs

1use std::collections::HashMap;
2
3use camel_api::CamelError;
4
5/// Parsed components of a Camel URI.
6///
7/// Format: `scheme:path?key1=value1&key2=value2`
8#[derive(Clone, PartialEq)]
9pub struct UriComponents {
10    /// The scheme (component name), e.g. "timer", "log".
11    pub scheme: String,
12    /// The path portion after the scheme, e.g. "tick" in "timer:tick".
13    pub path: String,
14    /// Query parameters as key-value pairs.
15    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
46/// Parse a Camel-style URI into its components.
47///
48/// Format: `scheme:path?key1=value1&key2=value2`
49pub 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}