config-manager 0.4.5

Crate to build config from environment, command line and files
Documentation
use std::collections::HashMap;

use config_manager::{config, ConfigInit};
use serde::Deserialize;

use crate::{assert_ok_and_compare, set_env, test_env};

fn file_not_found() {
    #[config(
        file(
            format = "json",
            env = "config_1",
            clap(long = "config_1"),
            default = "./fake.json",
        ),
        __debug_cmd_input__()
    )]
    struct FileNotFound {}

    assert!(matches!(
        FileNotFound::parse(),
        Err(config_manager::Error::ExternalError(_))
    ));

    set_env("config_1", "fake");
    assert!(matches!(
        FileNotFound::parse(),
        Err(config_manager::Error::ExternalError(_))
    ));

    set_env("config_1", "./tests/data/config.json");
    assert!(FileNotFound::parse().is_ok());
}

fn file_found() {
    #[derive(Debug, PartialEq)]
    #[config(
        file(format = "json", clap(long = "config1")),
        __debug_cmd_input__("--config1=./tests/data/config.json")
    )]
    struct ClapOnly {
        #[source(config)]
        int: i32,
    }

    assert_ok_and_compare(&ClapOnly { int: 1 });

    #[derive(Debug, PartialEq)]
    #[config(file(format = "json", env = "config_1"), __debug_cmd_input__())]
    struct EnvOnly {
        #[source(config)]
        int: i32,
    }

    set_env("config_1", "./tests/data/config.json");
    assert_ok_and_compare(&EnvOnly { int: 1 });

    #[derive(Debug, PartialEq)]
    #[config(
        file(format = "json", default = "./tests/data/config.json"),
        __debug_cmd_input__()
    )]
    struct DefaultOnly {
        #[source(config)]
        int: i32,
    }

    assert_ok_and_compare(&DefaultOnly { int: 1 });

    #[derive(Debug, PartialEq)]
    #[config(
        file(
            format = "toml",
            clap(long = "config2"),
            env = "config_2",
            default = "./Cargo.toml",
        ),
        __debug_cmd_input__("--config2=./tests/data/config.toml")
    )]
    struct Both {
        #[source(config)]
        int: i32,
    }

    set_env("config_2", "./config-manager-proc/Cargo.toml");
    assert_ok_and_compare(&Both { int: 1 });
}

fn optional() {
    #[config(file(format = "json", env = "config_1",))]
    struct NotFound {}

    assert!(NotFound::parse().is_err());

    #[config(
        file(format = "json", env = "config_1", optional),
        __debug_cmd_input__()
    )]
    struct Optional {}

    assert!(Optional::parse().is_ok());
}

fn solo_json() {
    #[derive(Debug, Deserialize, PartialEq)]
    enum Foo {
        Var1,
        Var2(i32),
        Var3,
    }

    #[derive(Debug, Deserialize, PartialEq)]
    struct Bar {
        name: String,
        addr: String,
    }

    #[derive(Debug, PartialEq)]
    #[config(file(format = "json", env = "json_config"), __debug_cmd_input__())]
    struct Json {
        #[source(config)]
        int: i32,
        #[source(config = "name")]
        string: String,
        #[source(config = "10xPI")]
        float: f64,
        #[source(config)]
        none: Option<i64>,
        #[source(config)]
        some: Option<u64>,
        #[source(config = "debug_mode")]
        boolean: bool,
        #[source(config = "convenient_phone_map")]
        map: HashMap<String, u64>,
        #[source(config)]
        array_of_enums: Vec<Foo>,
        #[source(config)]
        class: Bar,
    }

    set_env("json_config", "./tests/data/config.json");
    assert_ok_and_compare(&Json {
        int: 1,
        string: "Mike".into(),
        float: 31.4159265,
        none: None,
        some: Some(999999999999999),
        boolean: true,
        map: HashMap::from([
            ("Mike".into(), 89650000000),
            ("James".into(), 79645553535),
            ("Money".into(), 89991113511),
        ]),
        array_of_enums: vec![Foo::Var1, Foo::Var3, Foo::Var3, Foo::Var2(5)],
        class: Bar {
            name: "Mike".into(),
            addr: "Moscow".into(),
        },
    });
}

fn toml_solo() {
    #[derive(Debug, Deserialize, PartialEq)]
    struct Fruit {
        name: String,
        physical: Option<HashMap<String, String>>,
        varieties: Vec<HashMap<String, String>>,
    }

    #[derive(Debug, PartialEq)]
    #[config(file(format = "toml", env = "toml_config"), __debug_cmd_input__())]
    struct Toml {
        #[source(config)]
        int: i32,
        #[source(config = "name")]
        string: String,
        #[source(config = "10xPI")]
        float: f64,
        #[source(config)]
        some: Option<u64>,
        #[source(config = "debug_mode")]
        boolean: bool,
        #[source(config)]
        fruits: Vec<Fruit>,
    }

    set_env("toml_config", "./tests/data/config.toml");
    assert_ok_and_compare(&Toml {
        int: 1,
        string: "Mike".into(),
        float: 31.4159265,
        some: Some(999999999999999),
        boolean: true,
        fruits: vec![
            Fruit {
                name: "apple".into(),
                physical: Some(HashMap::from([
                    ("color".into(), "red".into()),
                    ("shape".into(), "round".into()),
                ])),
                varieties: vec![
                    HashMap::from([("name".into(), "red delicious".into())]),
                    HashMap::from([("name".into(), "granny smith".into())]),
                ],
            },
            Fruit {
                name: "banana".into(),
                physical: None,
                varieties: vec![HashMap::from([("name".into(), "plantain".into())])],
            },
        ],
    });
}

fn yaml_solo() {
    #[derive(Debug, Deserialize, PartialEq)]
    struct EmployeeInfo {
        name: String,
        job: String,
        skills: Vec<String>,
    }

    #[derive(Debug, PartialEq)]
    #[config(file(format = "yaml", env = "yaml_config"), __debug_cmd_input__())]
    struct Yaml {
        #[source(config)]
        none: Option<i32>,
        #[source(config = "bool_list")]
        array: Vec<bool>,
        #[source(config = "employees")]
        workers: HashMap<String, EmployeeInfo>,
    }

    set_env("yaml_config", "./tests/data/config.yml");
    assert_ok_and_compare(&Yaml {
        none: None,
        array: vec![true, false, true, false],
        workers: HashMap::from([
            (
                "martin".into(),
                EmployeeInfo {
                    name: "Martin D'vloper".into(),
                    job: "Developer".into(),
                    skills: vec!["python".into(), "perl".into(), "pascal".into()],
                },
            ),
            (
                "tabitha".into(),
                EmployeeInfo {
                    name: "Tabitha Bitumen".into(),
                    job: "Developer".into(),
                    skills: vec!["lisp".into(), "fortran".into(), "erlang".into()],
                },
            ),
        ]),
    });
}

fn merged_configs() {
    #[derive(Debug, Deserialize, PartialEq, Clone)]
    struct Person {
        name: String,
        surname: String,
    }

    #[derive(Debug, PartialEq)]
    #[config(
        file(format = "toml", env = "toml_config"),
        file(format = "json", env = "json_config"),
        file(format = "yaml", env = "yaml_config"),
        __debug_cmd_input__()
    )]
    struct Merged {
        #[source(config = "json_person")]
        json: Person,
        #[source(config = "toml_person")]
        toml: Person,
        #[source(config = "yaml_person")]
        yml: Person,
        #[source(config)]
        json_toml: Person,
        #[source(config)]
        json_yml: Person,
        #[source(config)]
        toml_yml: Person,
        #[source(config = "person")]
        json_toml_yml: Person,
    }

    set_env("json_config", "./tests/data/config.json");
    set_env("toml_config", "./tests/data/config.toml");
    set_env("yaml_config", "./tests/data/config.yml");

    let chuck = Person {
        name: "Charles".into(),
        surname: "McGill".into(),
    };
    let jimmy = Person {
        name: "James".into(),
        surname: "McGill".into(),
    };
    let saul = Person {
        name: "Saul".into(),
        surname: "Goodman".into(),
    };

    assert_ok_and_compare(&Merged {
        json: chuck.clone(),
        toml: jimmy,
        yml: saul.clone(),
        json_toml: chuck,
        json_yml: saul.clone(),
        toml_yml: saul.clone(),
        json_toml_yml: saul,
    });
}

#[test]
fn field_from_file() {
    test_env(vec![solo_json, toml_solo, yaml_solo, merged_configs]);
}

#[test]
fn file_attribute() {
    test_env(vec![optional, file_not_found, file_found]);
}