irx-config 2.1.0

The library provides convenient way to represent/parse configuration from different sources
Documentation
use crate::{json, AnyResult, ConfigBuilder, Value};
use std::fs;

#[macro_export]
macro_rules! resource_dir {
    () => {
        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/resources")
    };
}

#[macro_export]
macro_rules! resource_path {
    ($name:expr) => {
        concat!(resource_dir!(), "/", $name)
    };
}

#[cfg(feature = "env")]
mod env_test {
    use super::*;
    use crate::parsers::env::ParserBuilder;
    use std::env;

    fn setup() {
        env::set_var("HOME", "/home/joe");
        env::set_var("APP_ID", "42");
        env::set_var("APP_NODE1__ID", "1");
        env::set_var("APP_NODE1__NAMES", "[master, '1']");
    }

    #[test]
    fn parser() -> AnyResult<()> {
        setup();

        let expected = Value::try_from(json!(
        {
            "id": 42,
            "node1": {
                "id": 1,
                "names": ["master", "1"]
            }
        }))?;

        let parsers = [ParserBuilder::default()
            .default_prefix("APP_")
            .prefix_option("prefix")
            .build()?];
        let conf = ConfigBuilder::load_from(parsers)?;
        assert_eq!(expected, conf.get::<Value>()?);
        Ok(())
    }

    #[test]
    fn parser_case_sensitive() -> AnyResult<()> {
        setup();

        let expected = Value::try_from(json!(
        {
            "ID": 42,
            "NODE1": {
                "ID": 1,
                "NAMES": ["master", "1"]
            }
        }))?;

        let parser = ParserBuilder::default()
            .default_prefix("APP_")
            .prefix_option("prefix")
            .case_sensitive(true)
            .build()?;
        let conf = ConfigBuilder::load_one(parser)?;
        assert_eq!(expected, conf.get::<Value>()?);
        Ok(())
    }
}

#[cfg(feature = "json")]
mod json_test {
    use super::*;
    use crate::parsers::json::ParserBuilder;

    #[test]
    fn parser() -> AnyResult<()> {
        let path = resource_path!("config.json");
        let expected = fs::read_to_string(&path)?;
        let expected: Value = serde_json::from_str(&expected)?;

        let conf = ConfigBuilder::default()
            .append_parser(
                ParserBuilder::default()
                    .default_path(path)
                    .path_option("config")
                    .build()?,
            )
            .load()?;
        assert_eq!(expected, conf.get::<Value>()?);
        Ok(())
    }
}

#[cfg(feature = "yaml")]
mod yaml_test {
    use super::*;
    use crate::parsers::yaml::ParserBuilder;
    use std::path::Path;

    #[test]
    fn parser() -> AnyResult<()> {
        let path = Path::new(resource_dir!()).join("config.yaml");
        let expected = fs::read_to_string(&path)?;
        let expected: Value = serde_yaml::from_str(&expected)?;

        let conf =
            ConfigBuilder::load_from(vec![ParserBuilder::default().default_path(path).build()?])?;
        assert_eq!(expected, conf.get::<Value>()?);
        Ok(())
    }
}

#[cfg(feature = "toml-parser")]
mod toml_test {
    use super::*;
    use crate::parsers::toml::ParserBuilder;

    #[test]
    fn parser() -> AnyResult<()> {
        let path = resource_path!("config.toml");
        let expected = Value::try_from(json!({
            "id": 42,
            "node1": {
                "id": 1,
                "names": ["master", "1"],
                "timestamp": "2021-09-12T20:11:45Z",
                "date": "2021-09-12",
                "time": "20:11:45",
                "factor": 1.3
            },
            "persons": [
                {
                    "name": "John Doe",
                    "age": 42
                },
                {
                    "name": "Jane Doe",
                    "age": 24
                }
            ]
        }))?;
        println!("expected: {:?}", expected);

        let conf = ConfigBuilder::load_one(
            ParserBuilder::default()
                .default_path(path)
                .path_option("config")
                .build()?,
        )?;
        let calculated: Value = conf.get()?;
        println!("calculated: {:?}", calculated);

        assert_eq!(expected, calculated);
        Ok(())
    }
}

#[cfg(feature = "cmd")]
mod test_cmd {
    use super::*;
    use crate::parsers::cmd::ParserBuilder;
    use clap::{Arg, Command};

    fn create_app_with_subcmds<'a>() -> Command<'a> {
        let user_args = [
            Arg::new("name:first").short('n').takes_value(true),
            Arg::new("enabled").short('e').global(true),
        ];

        let alias_args = [
            Arg::new("name").short('a').takes_value(true).required(true),
            Arg::new("enabled").short('e').global(true),
        ];

        Command::new("test")
            .arg(
                Arg::new("config")
                    .short('c')
                    .long("config")
                    .takes_value(true)
                    .required(true),
            )
            .subcommand(
                Command::new("user")
                    .subcommand(Command::new("add").args(&user_args))
                    .subcommand(Command::new("del").args(&user_args)),
            )
            .subcommand(
                Command::new("alias")
                    .subcommand(Command::new("add").args(&alias_args))
                    .subcommand(Command::new("del").args(&alias_args)),
            )
    }

    #[test]
    fn parser() -> AnyResult<()> {
        let expected = Value::try_from(json!({
            "name": "Joe",
            "timeout": ["", 1000, "2000", false],
            "verbose": 3,
            "tag": ""
        }))?;
        println!("expected: {:?}", expected);

        let command = Command::new("test").args([
            Arg::new("name")
                .short('n')
                .long("name")
                .takes_value(true)
                .required(true),
            Arg::new("timeout")
                .multiple_occurrences(true)
                .short('t')
                .long("timeout")
                .takes_value(true),
            Arg::new("tag").short('T').takes_value(true),
            Arg::new("verbose").multiple_occurrences(true).short('v'),
        ]);
        let args = [
            "test",
            "-n",
            "Joe",
            "-t",
            "''",
            "-t",
            "1000",
            "-v",
            "--timeout",
            "'2000'",
            "-t",
            "false",
            "-vv",
            "-T",
            "''",
        ];
        let conf = ConfigBuilder::load_one(ParserBuilder::new(command).args(args).build()?)?;
        let calculated = conf.get_value();
        println!("calculated: {:?}", calculated);
        assert_eq!(expected, *calculated);
        Ok(())
    }

    fn user_add(expected: Value, global_on: bool) -> AnyResult<()> {
        println!("expected: {:?}", expected);

        let app = create_app_with_subcmds();
        let args = [
            "test",
            "--config",
            "config.toml",
            "user",
            "add",
            "-n",
            "John",
            "-e",
        ];
        let conf = ConfigBuilder::load_one(
            ParserBuilder::new(app)
                .args(args)
                .global_key_names(global_on)
                .build()?,
        )?;
        let calculated = conf.get_value();
        println!("calculated: {:?}", calculated);
        assert_eq!(expected, *calculated);
        Ok(())
    }

    #[test]
    fn user_add_without_global_names() -> AnyResult<()> {
        let expected = Value::try_from(json!({
            "config": "config.toml",
            "user": {
                "add": {
                    "name": {
                        "first": "John"
                    },
                    "enabled": 1
                }
            }
        }))?;

        user_add(expected, false)
    }

    #[test]
    fn user_add_with_global_names() -> AnyResult<()> {
        let expected = Value::try_from(json!({
            "config": "config.toml",
            "name": {
                "first": "John"
            },
            "enabled": 1
        }))?;

        user_add(expected, true)
    }
}