1pub mod prelude;
2
3pub use prelude::*;
4pub use types::password::Password;
5
6mod core;
7mod errors;
8mod types;
9mod validators;
10
11#[cfg(test)]
12mod tests {
13 use super::*;
14 use easy_config_macros::EasyConfig;
15 use once_cell::sync::Lazy;
16 use std::collections::HashMap;
17 use std::fmt::Debug;
18
19 const H: &str = "h";
20 const DOC: &str = "Docs for 'a'. ";
21
22 #[test]
23 fn test_basic_types() {
24 #[derive(Debug, PartialEq, EasyConfig)]
25 struct TestConfig {
26 #[attr(default = 5, validator = Range::between(0, 14), importance = Importance::HIGH,
27 documentation = format!("{DOC} Must be between 0 and 14."))]
28 a: i32,
29 #[attr(importance = Importance::HIGH, documentation = "docs".to_string(),
30 group = "group")]
31 b: i64,
32 #[attr(default = "hello".to_string(), importance = Importance::HIGH, documentation = "docs")]
33 c: String,
34 #[attr(importance = Importance::HIGH, documentation = "docs")]
35 d: Vec<String>,
36 #[attr(importance = Importance::HIGH, documentation = "docs")]
37 e: f64,
38 #[attr(importance = Importance::HIGH, documentation = "docs")]
39 f: String,
40 #[attr(name = "prop.f", importance = Importance::HIGH, documentation = "docs")]
41 f1: String,
42 #[attr(importance = Importance::HIGH, documentation = "docs")]
43 g: bool,
44 #[attr(name=H, importance = Importance::HIGH, documentation = "docs")]
45 h: bool,
46 #[attr(importance = Importance::HIGH, documentation = "docs")]
47 i: bool,
48 #[attr(importance = Importance::HIGH, documentation = "docs")]
49 j: Password,
50 }
51
52 let mut props = HashMap::new();
54 props.insert("a".to_string(), "1 ".to_string());
55 props.insert("b".to_string(), "2".to_string());
56 props.insert("d".to_string(), " a , b, c".to_string());
58 props.insert("e".to_string(), "42.5".to_string());
59 props.insert("f".to_string(), "java.lang.String".to_string());
60 props.insert("prop.f".to_string(), "prop_f_val".to_string());
61 props.insert("g".to_string(), "true".to_string());
62 props.insert("h".to_string(), "FalSE".to_string());
63 props.insert("i".to_string(), "TRUE".to_string());
64 props.insert("j".to_string(), "password".to_string());
65
66 let config = TestConfig::from_props(&props).unwrap();
68
69 assert_eq!(config.a, 1);
71 assert_eq!(config.b, 2);
72 assert_eq!(config.c, "hello"); assert_eq!(config.d, vec!["a", "b", "c"]);
74 assert_eq!(config.e, 42.5);
75 assert_eq!(config.f, "java.lang.String");
76 assert_eq!(config.f1, "prop_f_val");
77 assert_eq!(config.g, true);
78 assert_eq!(config.h, false);
79 assert_eq!(config.i, true);
80 assert_eq!(config.j, Password::new("password".to_string()));
81 assert_eq!(config.j.to_string(), "[hidden]");
82 }
83
84 #[test]
85 fn test_can_add_internal_config() {
86 const CONFIG_NAME: &'static str = "internal.config";
87 #[derive(Debug, PartialEq, EasyConfig)]
88 struct TestConfig {
89 #[attr(name = CONFIG_NAME, importance = Importance::LOW)]
90 val: String,
91 }
92
93 let mut props = HashMap::new();
94 props.insert(CONFIG_NAME.to_string(), "value".to_string());
95
96 let config = TestConfig::from_props(&props).unwrap();
97
98 assert_eq!(config.val, "value");
99 }
100
101 #[test]
102 fn test_can_add_lazy_internal_config() {
103 static LAZY_CONFIG_NAME: Lazy<String> = Lazy::new(|| "lazy.config.name".to_string());
104 #[derive(Debug, PartialEq, EasyConfig)]
105 struct TestConfig {
106 #[attr(name = LAZY_CONFIG_NAME, importance = Importance::LOW)]
107 val: String,
108 }
109
110 let mut props = HashMap::new();
111 props.insert(LAZY_CONFIG_NAME.to_string(), "value".to_string());
112
113 let config = TestConfig::from_props(&props).unwrap();
114
115 assert_eq!(config.val, "value");
116 }
117
118 #[test]
119 fn test_null_default() {
120 #[derive(EasyConfig, Debug, PartialEq)]
121 struct TestConfig {
122 #[attr(documentation = "docs")]
124 a: Option<i32>,
125 }
126
127 let config = TestConfig::from_props(&HashMap::new()).unwrap();
128
129 assert_eq!(config.a, None);
130 }
131
132 #[test]
133 fn test_missing_required() {
134 #[derive(EasyConfig)]
135 struct TestConfig {
136 #[attr(importance = Importance::HIGH, documentation = "docs")]
138 _a: i32,
139 }
140
141 let config = TestConfig::from_props(&HashMap::new());
142
143 assert!(matches!(config, Err(ConfigError::MissingName(s)) if s == "_a"));
144 }
145
146 #[test]
147 fn test_parsing_empty_default_value_for_string_field_should_succeed() {
148 #[derive(EasyConfig)]
149 struct TestConfig {
150 #[attr(default="".to_string(), importance = Importance::HIGH, documentation = "docs")]
152 _a: String,
153 }
154
155 let _ = TestConfig::from_props(&HashMap::new()).expect("parsing should succeed");
156 }
157
158 macro_rules! test_bad_inputs {
159 ($test_name:ident, $type:ty, $bad_values:expr) => {
161 #[test]
162 fn $test_name() {
163 #[derive(EasyConfig, Debug)]
164 struct TestConfig { _name: $type }
165
166 for &value in $bad_values {
167 let mut props = HashMap::new();
168 props.insert("_name".to_string(), value.to_string());
169
170 let result = TestConfig::from_props(&props);
171
172 assert!(
173 matches!(&result, Err(ConfigError::InvalidValue { name, .. }) if name == "_name"),
174 "Expected InvalidValue error for type '{}' with input '{}', but got {:?}",
175 stringify!($type),
176 value,
177 result
178 );
179 }
180 }
181 };
182 }
183
184 test_bad_inputs!(
185 test_bad_inputs_for_int,
186 i32,
187 &["hello", "42.5", "9223372036854775807"]
188 );
189
190 test_bad_inputs!(
191 test_bad_inputs_for_long,
192 i64,
193 &["hello", "42.5", "922337203685477580700"]
194 );
195
196 test_bad_inputs!(test_bad_inputs_for_double, f64, &["hello", "not-a-number"]);
197
198 test_bad_inputs!(
199 test_bad_inputs_for_boolean,
200 bool,
201 &["hello", "truee", "fals", "0", "1"]
202 );
203
204 #[test]
205 fn test_invalid_default_range() {
206 #[derive(Debug, EasyConfig)]
207 struct TestConfig {
208 #[attr(default=-1, validator=Range::between(0, 10),
209 importance = Importance::HIGH, documentation = "docs")]
210 _a: i32,
211 }
212
213 let config = TestConfig::from_props(&HashMap::new());
214
215 assert!(
216 matches!(&config, Err(ConfigError::ValidationFailed{name, message})
217 if name == "_a" && message.contains("Value -1 must be at least 0")
218 ),
219 "Expected ValidationFailed error, but got {:?}",
220 &config
221 );
222
223 println!("Received expected error: {:?}", &config.unwrap_err());
224 }
225
226 #[test]
227 fn test_invalid_default_string() {
228 #[derive(Debug, EasyConfig)]
229 struct TestConfig {
230 #[attr(default="bad".to_string(), validator=ValidString::in_list(&["valid", "values"]),
231 importance = Importance::HIGH, documentation = "docs")]
232 _a: String,
233 }
234
235 let config = TestConfig::from_props(&HashMap::new());
236
237 assert!(
238 matches!(
239 &config,
240 Err(ConfigError::ValidationFailed { name, message })
241 if name == "_a" && message.contains("must be one of: valid, values")
242 ),
243 "Expected ValidationFailed error, but got {:?}",
244 &config
245 );
246
247 println!("Received expected error: {:?}", &config.unwrap_err());
248 }
249
250 macro_rules! test_validators {
260 ($test_name:ident, $type:ty, $default:expr, $validator:expr, $ok_values:expr, $bad_values:expr) => {
263 #[test]
264 fn $test_name() {
265 #[derive(Debug, EasyConfig)]
266 struct TestConfig {
267 #[attr(default = $default, validator = $validator, importance = Importance::HIGH,
268 documentation = "docs")]
269 name: $type,
270 }
271
272 for &value in $ok_values {
273 let mut props = HashMap::new();
274 props.insert("name".to_string(), value.to_string());
275
276 let config = TestConfig::from_props(&props).unwrap_or_else(|e| {
277 panic!("Expected success for input '{}', but got error: {}", value, e)
278 });
279
280 let expected_val = <$type as ConfigValue>::parse("name", value).unwrap();
281 assert_eq!(config.name, expected_val);
282 }
283
284 for &value in $bad_values {
285 let mut props = HashMap::new();
286 props.insert("name".to_string(), value.to_string());
287
288 let result = TestConfig::from_props(&props);
289
290 assert!(
291 matches!(&result, Err(ConfigError::ValidationFailed { name, .. }) if name == "name"),
292 "Expected ValidationFailed error for type '{}' with input '{}', but got {:?}",
293 stringify!($type),
294 value,
295 result
296 );
297 }
298 }
299 };
300 }
301
302 test_validators!(
303 test_range_validator,
304 i32,
305 1,
306 Range::between(0, 10),
307 &["1", "5", "9"],
308 &["-1", "11"]
309 );
310
311 test_validators!(
312 test_string_validator,
313 String,
314 "default".to_string(),
315 ValidString::in_list(&["good", "values", "default"]),
316 &["good", "values", "default"],
317 &["bad", "inputs", "DEFAULT"]
318 );
319
320 test_validators!(
321 test_list_validator,
322 Vec<String>,
323 vec!["1".to_string()],
324 ValidList::in_list(&["1", "2", "3"]),
325 &["1", "2", "3"],
326 &["4", "5", "6"]
327 );
328
329 #[test]
330 fn test_list_validator_any_non_duplicate_values() {
331 let allow_any_non_duplicate_values = ValidList::any_non_duplicate_values(true);
332
333 allow_any_non_duplicate_values
334 .validate("test.config", "a, b, c")
335 .unwrap();
336 allow_any_non_duplicate_values
337 .validate("test.config", "")
338 .unwrap();
339
340 #[derive(EasyConfig, Debug)]
342 struct TestConfig {
343 #[attr(validator = ValidList::any_non_duplicate_values(true))]
344 v: Option<Vec<String>>, }
346 let config = TestConfig::from_props(&HashMap::new()).unwrap();
347 assert_eq!(config.v, None);
348
349 let res = allow_any_non_duplicate_values.validate("test.config", "a, a");
350 assert!(
351 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
352 .eq("Validation failed for name 'test.config': \
353 Configuration 'test.config' values must not be duplicated.")),
354 "Expected ValidationFailed error but got {:?}",
355 &res
356 );
357
358 let res = allow_any_non_duplicate_values.validate("test.config", "a,,b"); assert!(
360 matches!(&res, Err(ConfigError::ValidationFailed{..})
361 if res.as_ref().unwrap_err().to_string().eq("Validation failed for name 'test.config': \
362 Configuration 'test.config' values must not be empty.")),
363 "Expected ValidationFailed error but got {:?}",
364 &res
365 );
366
367 let allow_any_non_duplicate_values = ValidList::any_non_duplicate_values(false);
368
369 allow_any_non_duplicate_values
370 .validate("test.config", "a, b, c")
371 .unwrap();
372
373 let res = allow_any_non_duplicate_values.validate("test.config", "");
374 assert!(
375 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
376 .eq("Validation failed for name 'test.config': \
377 Configuration 'test.config' must not be empty. Valid values include: any non-empty value")),
378 "Expected ValidationFailed error but got {:?}",
379 &res
380 );
381
382 let res = allow_any_non_duplicate_values.validate("test.config", "a, a");
383 assert!(
384 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
385 .eq("Validation failed for name 'test.config': \
386 Configuration 'test.config' values must not be duplicated.")),
387 "Expected ValidationFailed error but got {:?}",
388 &res
389 );
390
391 let res = allow_any_non_duplicate_values.validate("test.config", "a,,b"); assert!(
393 matches!(&res, Err(ConfigError::ValidationFailed{..})
394 if res.as_ref().unwrap_err().to_string().eq("Validation failed for name 'test.config': \
395 Configuration 'test.config' values must not be empty.")),
396 "Expected ValidationFailed error but got {:?}",
397 &res
398 );
399 }
400
401 #[test]
402 fn test_list_validator_in() {
403 let allow_empty_validator = ValidList::in_list(&["a", "b", "c"]);
404
405 allow_empty_validator
406 .validate("test.config", "a, b")
407 .unwrap();
408 allow_empty_validator.validate("test.config", "").unwrap();
409
410 let res = allow_empty_validator.validate("test.config", "d");
411 assert!(
412 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
413 .eq("Validation failed for name 'test.config': \
414 Invalid value 'd' for configuration 'test.config': String must be one of: a, b, c")),
415 "Expected ValidationFailed error but got {:?}",
416 &res
417 );
418
419 let res = allow_empty_validator.validate("test.config", "a, a");
420 assert!(
421 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
422 .eq("Validation failed for name 'test.config': \
423 Configuration 'test.config' values must not be duplicated.")),
424 "Expected ValidationFailed error but got {:?}",
425 &res
426 );
427
428 let res = allow_empty_validator.validate("test.config", "a,,b"); assert!(
430 matches!(&res, Err(ConfigError::ValidationFailed{..})
431 if res.as_ref().unwrap_err().to_string().eq("Validation failed for name 'test.config': \
432 Configuration 'test.config' values must not be empty.")),
433 "Expected ValidationFailed error but got {:?}",
434 &res
435 );
436
437 let not_allow_empty_validator = ValidList::in_list_allow_empty(false, &["a", "b", "c"]);
438
439 not_allow_empty_validator
440 .validate("test.config", "a, b")
441 .unwrap();
442
443 let res = not_allow_empty_validator.validate("test.config", "");
444 assert!(
445 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
446 .eq("Validation failed for name 'test.config': \
447 Configuration 'test.config' must not be empty. Valid values include: [a, b, c] (empty config empty not allowed)")),
448 "Expected ValidationFailed error but got {:?}",
449 &res
450 );
451
452 let res = not_allow_empty_validator.validate("test.config", "a, a");
453 assert!(
454 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
455 .eq("Validation failed for name 'test.config': \
456 Configuration 'test.config' values must not be duplicated.")),
457 "Expected ValidationFailed error but got {:?}",
458 &res
459 );
460
461 let res = not_allow_empty_validator.validate("test.config", "d");
462 assert!(
463 matches!(&res, Err(ConfigError::ValidationFailed{..}) if res.as_ref().unwrap_err().to_string()
464 .eq("Validation failed for name 'test.config': \
465 Invalid value 'd' for configuration 'test.config': String must be one of: a, b, c")),
466 "Expected ValidationFailed error but got {:?}",
467 &res
468 );
469
470 let res = not_allow_empty_validator.validate("test.config", "a,,b"); assert!(
472 matches!(&res, Err(ConfigError::ValidationFailed{..})
473 if res.as_ref().unwrap_err().to_string().eq("Validation failed for name 'test.config': \
474 Configuration 'test.config' values must not be empty.")),
475 "Expected ValidationFailed error but got {:?}",
476 &res
477 );
478 }
479
480 #[test]
481 fn test_merge() {
482 mod test_conf1 {
483 use super::prelude::*;
484
485 #[derive(Debug, PartialEq, EasyConfig)]
486 pub struct TestConfig1 {
487 #[attr(default = 5, validator=Range::between(0, 14),
488 importance = Importance::HIGH, documentation = "docs", getter)]
489 a1: i32,
490 #[attr(default = "hello".to_string(), importance = Importance::HIGH, documentation = "docs",
491 getter)]
492 b1: String,
493 }
494 }
495
496 mod test_conf2 {
497 use super::prelude::*;
498
499 const A2_DEF_VAL: i32 = 5;
500 #[derive(Debug, PartialEq, EasyConfig)]
501 pub struct TestConfig2 {
502 #[attr(default = A2_DEF_VAL, validator=Range::between(0, 14),
503 importance = Importance::HIGH, documentation = "docs", getter)]
504 a2: i32,
505 #[attr(importance = Importance::HIGH, documentation = "docs", getter)]
506 b2: String,
507 }
508 }
509
510 #[derive(Debug, PartialEq, EasyConfig)]
511 struct MergeTestConfig {
512 #[merge]
513 config1: test_conf1::TestConfig1,
514 #[merge]
515 config2: test_conf2::TestConfig2,
516 }
517
518 let mut props = HashMap::new();
519 props.insert("a1".to_string(), "1 ".to_string());
520 props.insert("a2".to_string(), " 2 ".to_string());
521 props.insert("b2".to_string(), "value2".to_string());
523
524 let config = MergeTestConfig::from_props(&props).unwrap();
526
527 assert_eq!(config.config1.a1(), &1);
529 assert_eq!(config.config2.a2(), &2);
530 assert_eq!(config.config1.b1(), "hello");
531 assert_eq!(config.config2.b2(), "value2");
532 }
533}