Skip to main content

camel_endpoint/
config.rs

1use camel_api::CamelError;
2
3use crate::UriComponents;
4
5/// Trait for configuration types that can be parsed from Camel URIs.
6///
7/// This trait is typically implemented via the `#[derive(UriConfig)]` macro
8/// from `camel-endpoint-macros`.
9pub trait UriConfig: Sized {
10    /// Returns the URI scheme this config handles (e.g., "timer", "http").
11    fn scheme() -> &'static str;
12
13    /// Parse a URI string into this configuration.
14    fn from_uri(uri: &str) -> Result<Self, CamelError>;
15
16    /// Parse already-extracted URI components into this configuration.
17    fn from_components(parts: UriComponents) -> Result<Self, CamelError>;
18
19    /// Override to add validation logic after parsing.
20    fn validate(self) -> Result<Self, CamelError> {
21        Ok(self)
22    }
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28
29    #[test]
30    fn test_trait_exists() {
31        // Just verify the trait is defined
32        fn _uses_trait<T: UriConfig>() {}
33    }
34}
35
36#[cfg(test)]
37mod derive_tests {
38    use super::*;
39    use crate::UriConfig;
40
41    // Allow the derive macro to reference `camel_endpoint::` when used within this crate
42    extern crate self as camel_endpoint;
43
44    // Simple config with just path
45    #[derive(Debug, Clone, UriConfig)]
46    #[uri_scheme = "test"]
47    struct SimpleConfig {
48        name: String,
49    }
50
51    #[test]
52    fn test_simple_path_extraction() {
53        let config = SimpleConfig::from_uri("test:hello").unwrap();
54        assert_eq!(config.name, "hello");
55    }
56
57    #[test]
58    fn test_simple_scheme() {
59        assert_eq!(SimpleConfig::scheme(), "test");
60    }
61
62    // Config with parameters and defaults
63    #[derive(Debug, Clone, UriConfig)]
64    #[uri_scheme = "test"]
65    struct ConfigWithParams {
66        name: String,
67        #[uri_param(default = "1000")]
68        timeout: u64,
69        #[uri_param(default = "true")]
70        enabled: bool,
71    }
72
73    #[test]
74    fn test_params_with_defaults() {
75        let config = ConfigWithParams::from_uri("test:foo?timeout=500").unwrap();
76        assert_eq!(config.name, "foo");
77        assert_eq!(config.timeout, 500);
78        assert!(config.enabled); // uses default
79    }
80
81    #[test]
82    fn test_params_all_specified() {
83        let config = ConfigWithParams::from_uri("test:bar?timeout=2000&enabled=false").unwrap();
84        assert_eq!(config.name, "bar");
85        assert_eq!(config.timeout, 2000);
86        assert!(!config.enabled);
87    }
88
89    #[test]
90    fn test_scheme_validation() {
91        let result = SimpleConfig::from_uri("wrong:hello");
92        assert!(result.is_err());
93        if let Err(CamelError::InvalidUri(msg)) = result {
94            assert!(msg.contains("expected scheme 'test'"));
95            assert!(msg.contains("got 'wrong'"));
96        } else {
97            panic!("Expected InvalidUri error");
98        }
99    }
100
101    // Config with Option fields
102    #[derive(Debug, Clone, UriConfig)]
103    #[uri_scheme = "timer"]
104    struct TimerConfig {
105        timer_name: String,
106        #[uri_param]
107        period: Option<u64>,
108        #[uri_param]
109        repeat: Option<bool>,
110        #[uri_param]
111        description: Option<String>,
112    }
113
114    #[test]
115    fn test_option_params_present() {
116        let config =
117            TimerConfig::from_uri("timer:tick?period=1000&repeat=true&description=hello").unwrap();
118        assert_eq!(config.timer_name, "tick");
119        assert_eq!(config.period, Some(1000));
120        assert_eq!(config.repeat, Some(true));
121        assert_eq!(config.description, Some("hello".to_string()));
122    }
123
124    #[test]
125    fn test_option_params_absent() {
126        let config = TimerConfig::from_uri("timer:tick").unwrap();
127        assert_eq!(config.timer_name, "tick");
128        assert_eq!(config.period, None);
129        assert_eq!(config.repeat, None);
130        assert_eq!(config.description, None);
131    }
132
133    // Config with custom param names
134    #[derive(Debug, Clone, UriConfig)]
135    #[uri_scheme = "http"]
136    struct HttpConfig {
137        url: String,
138        #[uri_param(name = "httpMethod")]
139        method: Option<String>,
140        #[uri_param(name = "connectTimeout", default = "5000")]
141        timeout_ms: u64,
142    }
143
144    #[test]
145    fn test_custom_param_names() {
146        let config =
147            HttpConfig::from_uri("http://example.com?httpMethod=POST&connectTimeout=10000")
148                .unwrap();
149        assert_eq!(config.url, "//example.com");
150        assert_eq!(config.method, Some("POST".to_string()));
151        assert_eq!(config.timeout_ms, 10000);
152    }
153
154    #[test]
155    fn test_custom_param_name_default() {
156        let config = HttpConfig::from_uri("http://example.com").unwrap();
157        assert_eq!(config.url, "//example.com");
158        assert_eq!(config.method, None);
159        assert_eq!(config.timeout_ms, 5000); // default
160    }
161
162    // Config with multiple numeric types
163    #[derive(Debug, Clone, UriConfig)]
164    #[uri_scheme = "data"]
165    struct NumericConfig {
166        path: String,
167        #[uri_param(default = "100")]
168        count_u32: u32,
169        #[uri_param(default = "1000")]
170        count_u64: u64,
171        #[uri_param(default = "10")]
172        count_usize: usize,
173        #[uri_param(default = "-5")]
174        offset_i32: i32,
175    }
176
177    #[test]
178    fn test_numeric_types() {
179        let config = NumericConfig::from_uri(
180            "data:test?count_u32=50&count_u64=500&count_usize=5&offset_i32=-10",
181        )
182        .unwrap();
183        assert_eq!(config.path, "test");
184        assert_eq!(config.count_u32, 50);
185        assert_eq!(config.count_u64, 500);
186        assert_eq!(config.count_usize, 5);
187        assert_eq!(config.offset_i32, -10);
188    }
189
190    #[test]
191    fn test_numeric_defaults() {
192        let config = NumericConfig::from_uri("data:test").unwrap();
193        assert_eq!(config.count_u32, 100);
194        assert_eq!(config.count_u64, 1000);
195        assert_eq!(config.count_usize, 10);
196        assert_eq!(config.offset_i32, -5);
197    }
198
199    #[test]
200    fn test_invalid_numeric_value() {
201        let result = NumericConfig::from_uri("data:test?count_u32=abc");
202        assert!(result.is_err());
203    }
204
205    // Test from_components directly
206    #[test]
207    fn test_from_components() {
208        let components = UriComponents {
209            scheme: "test".to_string(),
210            path: "hello".to_string(),
211            params: std::collections::HashMap::from([
212                ("timeout".to_string(), "500".to_string()),
213                ("enabled".to_string(), "false".to_string()),
214            ]),
215        };
216
217        let config = ConfigWithParams::from_components(components).unwrap();
218        assert_eq!(config.name, "hello");
219        assert_eq!(config.timeout, 500);
220        assert!(!config.enabled);
221    }
222
223    // Test with validate
224    #[test]
225    fn test_validate_passthrough() {
226        let config = SimpleConfig::from_uri("test:hello")
227            .unwrap()
228            .validate()
229            .unwrap();
230        assert_eq!(config.name, "hello");
231    }
232
233    // Test: Issue 1 - Non-Option bool without default should error when missing
234    #[derive(Debug, Clone, UriConfig)]
235    #[uri_scheme = "feature"]
236    struct FeatureConfig {
237        feature_name: String,
238        #[uri_param]
239        enabled: bool, // No default - should require the parameter
240    }
241
242    #[test]
243    fn test_bool_without_default_missing_errors() {
244        // Should error because 'enabled' is required but not provided
245        let result = FeatureConfig::from_uri("feature:test");
246        assert!(result.is_err());
247        if let Err(CamelError::InvalidUri(msg)) = result {
248            assert!(
249                msg.contains("missing required parameter"),
250                "Error should mention missing parameter, got: {}",
251                msg
252            );
253            assert!(msg.contains("enabled"), "Error should mention 'enabled'");
254        } else {
255            panic!("Expected InvalidUri error for missing bool parameter");
256        }
257    }
258
259    #[test]
260    fn test_bool_without_default_provided_works() {
261        let config = FeatureConfig::from_uri("feature:test?enabled=true").unwrap();
262        assert_eq!(config.feature_name, "test");
263        assert!(config.enabled);
264        let config = FeatureConfig::from_uri("feature:test?enabled=false").unwrap();
265        assert_eq!(config.feature_name, "test");
266        assert!(!config.enabled);
267    }
268
269    // Test: Issue 2 - Option<u64> with invalid value should return None, not Some(0)
270    #[test]
271    fn test_option_numeric_invalid_returns_none() {
272        // Invalid numeric value should result in None, not Some(0)
273        let config = TimerConfig::from_uri("timer:tick?period=invalid").unwrap();
274        assert_eq!(
275            config.period, None,
276            "Invalid numeric value should return None, not Some(0)"
277        );
278    }
279
280    // Test: Issue 3 - Generic type fallback should include parse error in message
281    #[derive(Debug, Clone, PartialEq, Eq)]
282    enum TestEnum {
283        Alpha,
284        Beta,
285    }
286
287    impl std::str::FromStr for TestEnum {
288        type Err = String;
289
290        fn from_str(s: &str) -> Result<Self, Self::Err> {
291            match s {
292                "alpha" => Ok(TestEnum::Alpha),
293                "beta" => Ok(TestEnum::Beta),
294                _ => Err(format!("unknown variant: {}", s)),
295            }
296        }
297    }
298
299    #[derive(Debug, Clone, UriConfig)]
300    #[uri_scheme = "enumtest"]
301    struct EnumConfig {
302        path: String,
303        #[uri_param]
304        mode: TestEnum,
305    }
306
307    #[test]
308    fn test_enum_invalid_value_includes_error() {
309        let result = EnumConfig::from_uri("enumtest:foo?mode=invalid");
310        assert!(result.is_err());
311        if let Err(CamelError::InvalidUri(msg)) = result {
312            assert!(
313                msg.contains("invalid value"),
314                "Error should mention invalid value, got: {}",
315                msg
316            );
317            // The error should include the actual parse error from FromStr
318            assert!(
319                msg.contains("unknown variant"),
320                "Error should include parse error details, got: {}",
321                msg
322            );
323            assert!(
324                msg.contains("invalid"),
325                "Error should include the invalid value, got: {}",
326                msg
327            );
328        } else {
329            panic!("Expected InvalidUri error for invalid enum value");
330        }
331    }
332
333    #[test]
334    fn test_enum_valid_value_works() {
335        let config = EnumConfig::from_uri("enumtest:foo?mode=alpha").unwrap();
336        assert_eq!(config.path, "foo");
337        assert_eq!(config.mode, TestEnum::Alpha);
338        let config = EnumConfig::from_uri("enumtest:foo?mode=beta").unwrap();
339        assert_eq!(config.path, "foo");
340        assert_eq!(config.mode, TestEnum::Beta);
341    }
342
343    // Duration type support tests
344    #[derive(Debug, Clone, UriConfig)]
345    #[uri_scheme = "timer"]
346    struct TimerTestConfig {
347        name: String,
348
349        #[uri_param(default = "1000")]
350        period_ms: u64,
351
352        period: std::time::Duration,
353    }
354
355    #[test]
356    fn test_duration_from_ms_field() {
357        let config = TimerTestConfig::from_uri("timer:tick?period_ms=500").unwrap();
358        assert_eq!(config.name, "tick");
359        assert_eq!(config.period, std::time::Duration::from_millis(500));
360    }
361
362    #[test]
363    fn test_duration_uses_default() {
364        let config = TimerTestConfig::from_uri("timer:tick").unwrap();
365        assert_eq!(config.name, "tick");
366        assert_eq!(config.period_ms, 1000);
367        assert_eq!(config.period, std::time::Duration::from_millis(1000));
368    }
369
370    // Test Duration with multiple Duration fields
371    #[derive(Debug, Clone, UriConfig)]
372    #[uri_scheme = "scheduler"]
373    struct SchedulerConfig {
374        task_name: String,
375
376        #[uri_param(default = "5000")]
377        initial_delay_ms: u64,
378
379        #[uri_param(default = "10000")]
380        interval_ms: u64,
381
382        initial_delay: std::time::Duration,
383        interval: std::time::Duration,
384    }
385
386    #[test]
387    fn test_multiple_duration_fields() {
388        let config =
389            SchedulerConfig::from_uri("scheduler:cleanup?initial_delay_ms=2000&interval_ms=3000")
390                .unwrap();
391        assert_eq!(config.task_name, "cleanup");
392        assert_eq!(config.initial_delay_ms, 2000);
393        assert_eq!(config.interval_ms, 3000);
394        assert_eq!(config.initial_delay, std::time::Duration::from_millis(2000));
395        assert_eq!(config.interval, std::time::Duration::from_millis(3000));
396    }
397
398    #[test]
399    fn test_multiple_duration_defaults() {
400        let config = SchedulerConfig::from_uri("scheduler:cleanup").unwrap();
401        assert_eq!(config.task_name, "cleanup");
402        assert_eq!(config.initial_delay_ms, 5000);
403        assert_eq!(config.interval_ms, 10000);
404        assert_eq!(config.initial_delay, std::time::Duration::from_millis(5000));
405        assert_eq!(config.interval, std::time::Duration::from_millis(10000));
406    }
407}