1use camel_api::CamelError;
2
3use crate::UriComponents;
4
5pub trait UriConfig: Sized {
10 fn scheme() -> &'static str;
12
13 fn from_uri(uri: &str) -> Result<Self, CamelError>;
15
16 fn from_components(parts: UriComponents) -> Result<Self, CamelError>;
18
19 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 fn _uses_trait<T: UriConfig>() {}
33 }
34}
35
36#[cfg(test)]
37mod derive_tests {
38 use super::*;
39 use crate::UriConfig;
40
41 extern crate self as camel_endpoint;
43
44 #[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 #[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); }
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 #[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 #[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); }
161
162 #[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]
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]
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 #[derive(Debug, Clone, UriConfig)]
235 #[uri_scheme = "feature"]
236 struct FeatureConfig {
237 feature_name: String,
238 #[uri_param]
239 enabled: bool, }
241
242 #[test]
243 fn test_bool_without_default_missing_errors() {
244 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]
271 fn test_option_numeric_invalid_returns_error() {
272 let result = TimerConfig::from_uri("timer:tick?period=invalid");
274 assert!(result.is_err());
275 if let Err(CamelError::InvalidUri(msg)) = result {
276 assert!(
277 msg.contains("invalid value for period"),
278 "Error should mention the invalid param, got: {}",
279 msg
280 );
281 } else {
282 panic!("Expected InvalidUri error for invalid numeric Option value");
283 }
284 }
285
286 #[derive(Debug, Clone, UriConfig)]
288 #[uri_scheme = "booltest"]
289 struct BoolCaseConfig {
290 name: String,
291 #[uri_param]
292 flag: Option<bool>,
293 }
294
295 #[derive(Debug, Clone, UriConfig)]
296 #[uri_scheme = "booltest2"]
297 struct BoolDefaultConfig {
298 name: String,
299 #[uri_param(default = "false")]
300 enabled: bool,
301 }
302
303 #[test]
304 fn test_bool_case_insensitive_true_variants() {
305 for val in &["true", "True", "TRUE", "1", "yes", "Yes", "YES"] {
306 let uri = format!("booltest:foo?flag={}", val);
307 let config = BoolCaseConfig::from_uri(&uri).unwrap_or_else(|e| {
308 panic!("Failed to parse flag='{}' from URI '{}': {}", val, uri, e)
309 });
310 assert_eq!(
311 config.flag,
312 Some(true),
313 "flag='{}' should parse to Some(true)",
314 val
315 );
316 }
317 }
318
319 #[test]
320 fn test_bool_case_insensitive_false_variants() {
321 for val in &["false", "False", "FALSE", "0", "no", "No", "NO"] {
322 let uri = format!("booltest:foo?flag={}", val);
323 let config = BoolCaseConfig::from_uri(&uri).unwrap_or_else(|e| {
324 panic!("Failed to parse flag='{}' from URI '{}': {}", val, uri, e)
325 });
326 assert_eq!(
327 config.flag,
328 Some(false),
329 "flag='{}' should parse to Some(false)",
330 val
331 );
332 }
333 }
334
335 #[test]
336 fn test_bool_invalid_returns_error() {
337 let result = BoolCaseConfig::from_uri("booltest:foo?flag=maybe");
338 assert!(result.is_err());
339 if let Err(CamelError::InvalidUri(msg)) = result {
340 assert!(
341 msg.contains("invalid boolean value"),
342 "Error should mention invalid boolean, got: {}",
343 msg
344 );
345 } else {
346 panic!("Expected InvalidUri error for invalid bool value");
347 }
348 }
349
350 #[test]
351 fn test_bool_default_case_insensitive() {
352 for val in &["TRUE", "1", "YES"] {
354 let uri = format!("booltest2:bar?enabled={}", val);
355 let config = BoolDefaultConfig::from_uri(&uri).unwrap();
356 assert!(config.enabled, "enabled='{}' should be true", val);
357 }
358 for val in &["FALSE", "0", "NO"] {
359 let uri = format!("booltest2:bar?enabled={}", val);
360 let config = BoolDefaultConfig::from_uri(&uri).unwrap();
361 assert!(!config.enabled, "enabled='{}' should be false", val);
362 }
363 }
364
365 #[derive(Debug, Clone, PartialEq, Eq)]
367 enum TestEnum {
368 Alpha,
369 Beta,
370 }
371
372 impl std::str::FromStr for TestEnum {
373 type Err = String;
374
375 fn from_str(s: &str) -> Result<Self, Self::Err> {
376 match s {
377 "alpha" => Ok(TestEnum::Alpha),
378 "beta" => Ok(TestEnum::Beta),
379 _ => Err(format!("unknown variant: {}", s)),
380 }
381 }
382 }
383
384 #[derive(Debug, Clone, UriConfig)]
385 #[uri_scheme = "enumtest"]
386 struct EnumConfig {
387 path: String,
388 #[uri_param]
389 mode: TestEnum,
390 }
391
392 #[test]
393 fn test_enum_invalid_value_includes_error() {
394 let result = EnumConfig::from_uri("enumtest:foo?mode=invalid");
395 assert!(result.is_err());
396 if let Err(CamelError::InvalidUri(msg)) = result {
397 assert!(
398 msg.contains("invalid value"),
399 "Error should mention invalid value, got: {}",
400 msg
401 );
402 assert!(
404 msg.contains("unknown variant"),
405 "Error should include parse error details, got: {}",
406 msg
407 );
408 assert!(
409 msg.contains("invalid"),
410 "Error should include the invalid value, got: {}",
411 msg
412 );
413 } else {
414 panic!("Expected InvalidUri error for invalid enum value");
415 }
416 }
417
418 #[test]
419 fn test_enum_valid_value_works() {
420 let config = EnumConfig::from_uri("enumtest:foo?mode=alpha").unwrap();
421 assert_eq!(config.path, "foo");
422 assert_eq!(config.mode, TestEnum::Alpha);
423 let config = EnumConfig::from_uri("enumtest:foo?mode=beta").unwrap();
424 assert_eq!(config.path, "foo");
425 assert_eq!(config.mode, TestEnum::Beta);
426 }
427
428 #[derive(Debug, Clone, UriConfig)]
430 #[uri_scheme = "timer"]
431 struct TimerTestConfig {
432 name: String,
433
434 #[uri_param(default = "1000")]
435 period_ms: u64,
436
437 period: std::time::Duration,
438 }
439
440 #[test]
441 fn test_duration_from_ms_field() {
442 let config = TimerTestConfig::from_uri("timer:tick?period_ms=500").unwrap();
443 assert_eq!(config.name, "tick");
444 assert_eq!(config.period, std::time::Duration::from_millis(500));
445 }
446
447 #[test]
448 fn test_duration_uses_default() {
449 let config = TimerTestConfig::from_uri("timer:tick").unwrap();
450 assert_eq!(config.name, "tick");
451 assert_eq!(config.period_ms, 1000);
452 assert_eq!(config.period, std::time::Duration::from_millis(1000));
453 }
454
455 #[derive(Debug, Clone, UriConfig)]
457 #[uri_scheme = "scheduler"]
458 struct SchedulerConfig {
459 task_name: String,
460
461 #[uri_param(default = "5000")]
462 initial_delay_ms: u64,
463
464 #[uri_param(default = "10000")]
465 interval_ms: u64,
466
467 initial_delay: std::time::Duration,
468 interval: std::time::Duration,
469 }
470
471 #[test]
472 fn test_multiple_duration_fields() {
473 let config =
474 SchedulerConfig::from_uri("scheduler:cleanup?initial_delay_ms=2000&interval_ms=3000")
475 .unwrap();
476 assert_eq!(config.task_name, "cleanup");
477 assert_eq!(config.initial_delay_ms, 2000);
478 assert_eq!(config.interval_ms, 3000);
479 assert_eq!(config.initial_delay, std::time::Duration::from_millis(2000));
480 assert_eq!(config.interval, std::time::Duration::from_millis(3000));
481 }
482
483 #[test]
484 fn test_multiple_duration_defaults() {
485 let config = SchedulerConfig::from_uri("scheduler:cleanup").unwrap();
486 assert_eq!(config.task_name, "cleanup");
487 assert_eq!(config.initial_delay_ms, 5000);
488 assert_eq!(config.interval_ms, 10000);
489 assert_eq!(config.initial_delay, std::time::Duration::from_millis(5000));
490 assert_eq!(config.interval, std::time::Duration::from_millis(10000));
491 }
492}