Skip to main content

easy_config_def/
lib.rs

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        // Arrange: Set up the raw string properties.
53        let mut props = HashMap::new();
54        props.insert("a".to_string(), "1   ".to_string());
55        props.insert("b".to_string(), "2".to_string());
56        // "c" is omitted to test the default value.
57        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        // Act: Parse the properties into the strongly typed struct.
67        let config = TestConfig::from_props(&props).unwrap();
68
69        // Assert: Check the final parsed values.
70        assert_eq!(config.a, 1);
71        assert_eq!(config.b, 2);
72        assert_eq!(config.c, "hello"); // Correctly uses the default
73        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            // This field is optional and has no default.
123            #[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            // This field is required (not an Option, no default).
137            #[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            // This field is required empty by default.
151            #[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        // The macro takes a test name, the type to test, and a slice of bad values.
160        ($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    // TODO: Add support for pluggable components
251    //     @Test
252    //     public void testNestedClass() {
253    //         // getName(), not getSimpleName() or getCanonicalName(), is the version that should be able to locate the class
254    //         Map<String, Object> props = Collections.singletonMap("name", NestedClass.class.getName());
255    //         new ConfigDef().define("name", Type.CLASS, Importance.HIGH, "docs").parse(props);
256    //     }
257    //
258
259    macro_rules! test_validators {
260        // The macro takes a test name, type, validator, default, slice of ok values,
261        // slice of bad values.
262        ($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        // Test the "null allowed" case at the `from_props` level.
341        #[derive(EasyConfig, Debug)]
342        struct TestConfig {
343            #[attr(validator = ValidList::any_non_duplicate_values(true))]
344            v: Option<Vec<String>>, // `Option` makes it "null allowed"
345        }
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"); // Contains an empty string
359        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"); // Contains an empty string
392        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"); // Contains an empty string
429        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"); // Contains an empty string
471        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        // "b1" is omitted to test the default value.
522        props.insert("b2".to_string(), "value2".to_string());
523
524        // Act: Parse the properties into the strongly typed struct.
525        let config = MergeTestConfig::from_props(&props).unwrap();
526
527        // Assert: Check the final parsed values.
528        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}