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 raw_query: None,
216 };
217
218 let config = ConfigWithParams::from_components(components).unwrap();
219 assert_eq!(config.name, "hello");
220 assert_eq!(config.timeout, 500);
221 assert!(!config.enabled);
222 }
223
224 #[test]
226 fn test_validate_passthrough() {
227 let config = SimpleConfig::from_uri("test:hello")
228 .unwrap()
229 .validate()
230 .unwrap();
231 assert_eq!(config.name, "hello");
232 }
233
234 #[derive(Debug, Clone, UriConfig)]
236 #[uri_scheme = "feature"]
237 struct FeatureConfig {
238 feature_name: String,
239 #[uri_param]
240 enabled: bool, }
242
243 #[test]
244 fn test_bool_without_default_missing_errors() {
245 let result = FeatureConfig::from_uri("feature:test");
247 assert!(result.is_err());
248 if let Err(CamelError::InvalidUri(msg)) = result {
249 assert!(
250 msg.contains("missing required parameter"),
251 "Error should mention missing parameter, got: {}",
252 msg
253 );
254 assert!(msg.contains("enabled"), "Error should mention 'enabled'");
255 } else {
256 panic!("Expected InvalidUri error for missing bool parameter");
257 }
258 }
259
260 #[test]
261 fn test_bool_without_default_provided_works() {
262 let config = FeatureConfig::from_uri("feature:test?enabled=true").unwrap();
263 assert_eq!(config.feature_name, "test");
264 assert!(config.enabled);
265 let config = FeatureConfig::from_uri("feature:test?enabled=false").unwrap();
266 assert_eq!(config.feature_name, "test");
267 assert!(!config.enabled);
268 }
269
270 #[test]
272 fn test_option_numeric_invalid_returns_error() {
273 let result = TimerConfig::from_uri("timer:tick?period=invalid");
275 assert!(result.is_err());
276 if let Err(CamelError::InvalidUri(msg)) = result {
277 assert!(
278 msg.contains("invalid value for period"),
279 "Error should mention the invalid param, got: {}",
280 msg
281 );
282 } else {
283 panic!("Expected InvalidUri error for invalid numeric Option value");
284 }
285 }
286
287 #[derive(Debug, Clone, UriConfig)]
289 #[uri_scheme = "booltest"]
290 struct BoolCaseConfig {
291 #[allow(dead_code)]
292 name: String,
293 #[uri_param]
294 flag: Option<bool>,
295 }
296
297 #[derive(Debug, Clone, UriConfig)]
298 #[uri_scheme = "booltest2"]
299 struct BoolDefaultConfig {
300 #[allow(dead_code)]
301 name: String,
302 #[uri_param(default = "false")]
303 enabled: bool,
304 }
305
306 #[test]
307 fn test_bool_case_insensitive_true_variants() {
308 for val in &["true", "True", "TRUE", "1", "yes", "Yes", "YES"] {
309 let uri = format!("booltest:foo?flag={}", val);
310 let config = BoolCaseConfig::from_uri(&uri).unwrap_or_else(|e| {
311 panic!("Failed to parse flag='{}' from URI '{}': {}", val, uri, e)
312 });
313 assert_eq!(
314 config.flag,
315 Some(true),
316 "flag='{}' should parse to Some(true)",
317 val
318 );
319 }
320 }
321
322 #[test]
323 fn test_bool_case_insensitive_false_variants() {
324 for val in &["false", "False", "FALSE", "0", "no", "No", "NO"] {
325 let uri = format!("booltest:foo?flag={}", val);
326 let config = BoolCaseConfig::from_uri(&uri).unwrap_or_else(|e| {
327 panic!("Failed to parse flag='{}' from URI '{}': {}", val, uri, e)
328 });
329 assert_eq!(
330 config.flag,
331 Some(false),
332 "flag='{}' should parse to Some(false)",
333 val
334 );
335 }
336 }
337
338 #[test]
339 fn test_bool_invalid_returns_error() {
340 let result = BoolCaseConfig::from_uri("booltest:foo?flag=maybe");
341 assert!(result.is_err());
342 if let Err(CamelError::InvalidUri(msg)) = result {
343 assert!(
344 msg.contains("invalid boolean value"),
345 "Error should mention invalid boolean, got: {}",
346 msg
347 );
348 } else {
349 panic!("Expected InvalidUri error for invalid bool value");
350 }
351 }
352
353 #[test]
354 fn test_bool_default_case_insensitive() {
355 for val in &["TRUE", "1", "YES"] {
357 let uri = format!("booltest2:bar?enabled={}", val);
358 let config = BoolDefaultConfig::from_uri(&uri).unwrap();
359 assert!(config.enabled, "enabled='{}' should be true", val);
360 }
361 for val in &["FALSE", "0", "NO"] {
362 let uri = format!("booltest2:bar?enabled={}", val);
363 let config = BoolDefaultConfig::from_uri(&uri).unwrap();
364 assert!(!config.enabled, "enabled='{}' should be false", val);
365 }
366 }
367
368 #[derive(Debug, Clone, PartialEq, Eq)]
370 enum TestEnum {
371 Alpha,
372 Beta,
373 }
374
375 impl std::str::FromStr for TestEnum {
376 type Err = String;
377
378 fn from_str(s: &str) -> Result<Self, Self::Err> {
379 match s {
380 "alpha" => Ok(TestEnum::Alpha),
381 "beta" => Ok(TestEnum::Beta),
382 _ => Err(format!("unknown variant: {}", s)),
383 }
384 }
385 }
386
387 #[derive(Debug, Clone, UriConfig)]
388 #[uri_scheme = "enumtest"]
389 struct EnumConfig {
390 path: String,
391 #[uri_param]
392 mode: TestEnum,
393 }
394
395 #[test]
396 fn test_enum_invalid_value_includes_error() {
397 let result = EnumConfig::from_uri("enumtest:foo?mode=invalid");
398 assert!(result.is_err());
399 if let Err(CamelError::InvalidUri(msg)) = result {
400 assert!(
401 msg.contains("invalid value"),
402 "Error should mention invalid value, got: {}",
403 msg
404 );
405 assert!(
407 msg.contains("unknown variant"),
408 "Error should include parse error details, got: {}",
409 msg
410 );
411 assert!(
412 msg.contains("invalid"),
413 "Error should include the invalid value, got: {}",
414 msg
415 );
416 } else {
417 panic!("Expected InvalidUri error for invalid enum value");
418 }
419 }
420
421 #[test]
422 fn test_enum_valid_value_works() {
423 let config = EnumConfig::from_uri("enumtest:foo?mode=alpha").unwrap();
424 assert_eq!(config.path, "foo");
425 assert_eq!(config.mode, TestEnum::Alpha);
426 let config = EnumConfig::from_uri("enumtest:foo?mode=beta").unwrap();
427 assert_eq!(config.path, "foo");
428 assert_eq!(config.mode, TestEnum::Beta);
429 }
430
431 #[derive(Debug, Clone, UriConfig)]
433 #[uri_scheme = "timer"]
434 struct TimerTestConfig {
435 name: String,
436
437 #[uri_param(default = "1000")]
438 period_ms: u64,
439
440 period: std::time::Duration,
441 }
442
443 #[test]
444 fn test_duration_from_ms_field() {
445 let config = TimerTestConfig::from_uri("timer:tick?period_ms=500").unwrap();
446 assert_eq!(config.name, "tick");
447 assert_eq!(config.period, std::time::Duration::from_millis(500));
448 }
449
450 #[test]
451 fn test_duration_uses_default() {
452 let config = TimerTestConfig::from_uri("timer:tick").unwrap();
453 assert_eq!(config.name, "tick");
454 assert_eq!(config.period_ms, 1000);
455 assert_eq!(config.period, std::time::Duration::from_millis(1000));
456 }
457
458 #[derive(Debug, Clone, UriConfig)]
460 #[uri_scheme = "scheduler"]
461 struct SchedulerConfig {
462 task_name: String,
463
464 #[uri_param(default = "5000")]
465 initial_delay_ms: u64,
466
467 #[uri_param(default = "10000")]
468 interval_ms: u64,
469
470 initial_delay: std::time::Duration,
471 interval: std::time::Duration,
472 }
473
474 #[test]
475 fn test_multiple_duration_fields() {
476 let config =
477 SchedulerConfig::from_uri("scheduler:cleanup?initial_delay_ms=2000&interval_ms=3000")
478 .unwrap();
479 assert_eq!(config.task_name, "cleanup");
480 assert_eq!(config.initial_delay_ms, 2000);
481 assert_eq!(config.interval_ms, 3000);
482 assert_eq!(config.initial_delay, std::time::Duration::from_millis(2000));
483 assert_eq!(config.interval, std::time::Duration::from_millis(3000));
484 }
485
486 #[test]
487 fn test_multiple_duration_defaults() {
488 let config = SchedulerConfig::from_uri("scheduler:cleanup").unwrap();
489 assert_eq!(config.task_name, "cleanup");
490 assert_eq!(config.initial_delay_ms, 5000);
491 assert_eq!(config.interval_ms, 10000);
492 assert_eq!(config.initial_delay, std::time::Duration::from_millis(5000));
493 assert_eq!(config.interval, std::time::Duration::from_millis(10000));
494 }
495}