1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#![doc = include_str!("../README.md")]
#![doc(test(no_crate_inject))]

pub mod entity;
pub mod error;
pub mod loader;
pub mod parser;

#[doc(inline)]
pub use configuration::Configuration;
#[doc(inline)]
pub use error::ConfigurationError;

pub mod ext {
    //! Extern other crates.

    pub extern crate anyhow;
    pub extern crate plugx_input;
    pub extern crate serde;
    pub extern crate url;
}

mod configuration;
mod logging;

#[cfg(test)]
mod tests {
    use crate::logging::enable_logging;
    use crate::Configuration;
    use std::{collections::HashMap, env, fs};
    use tempdir::TempDir;
    use url::Url;

    #[test]
    fn smoke() {
        enable_logging();

        // In this example we're going to load our plugins' configurations from
        // a directory and environment-variables.
        // Here we have 4 plugins `foo`, `bar`, `baz`, and `qux`.

        // Set some configurations in environment-variables:
        env::set_var("APP_NAME__FOO__SERVER__ADDRESS", "127.0.0.1");
        env::set_var("APP_NAME__BAR__SQLITE__FILE", "/path/to/app.db");
        env::set_var("APP_NAME__BAZ__LOGGING__LEVEL", "debug");
        env::set_var("APP_NAME__QUX__HTTPS__INSECURE", "false");

        // Create a temporary directory `/tmp/.../etc/app-name` (which will be removed after running our example)
        let root_tmp = TempDir::new("example").expect("Create temporary directory");
        let cfg_dir = root_tmp.path().join("etc").join("app.d");
        fs::create_dir_all(cfg_dir.clone()).unwrap();
        // Write some configurations inside and example directory `/tmp/.../etc/app.d/`:
        fs::write(
            cfg_dir.join("foo.env"),
            "SERVER__PORT=8080 # This is a comment",
        )
        .unwrap();
        fs::write(
            cfg_dir.join("bar.json"),
            "{\"sqlite\": {\"recreate\": true}}",
        )
        .unwrap();
        fs::write(
            cfg_dir.join("baz.toml"),
            "[logging]\noutput_serialize_format = \"json\"",
        )
        .unwrap();
        fs::write(
            cfg_dir.join("qux.yaml"),
            "https:\n  follow_redirects: false",
        )
        .unwrap();

        // Create a URL for our environment-variables configuration:
        let env_url: Url = "env://?prefix=APP_NAME__&key_separator=__"
            .parse()
            .expect("Valid URL");
        // Create a URL for our `/tmp/.../etc/app.d/` directory:
        // `skippable` query-string key is list of skippable error names.
        // Here we want to skip `not found` error if the directory does not exists:
        let file_url: Url = format!("file://{}?skippable[0]=notfound", cfg_dir.to_str().unwrap())
            .parse()
            .expect("Valid URL");

        // We want to check our plugins' configurations for them but we do not know what they want!
        // We can load them and ask them what keys and values they expect to have before loading
        // and checking configurations.
        // Here for example we asked them about their configuration and collected their rules in
        // JSON format:
        let rules_json = r#"
        {
            "foo": {
                "type": "static_map",
                "definitions": {
                    "server": {
                        "definition": {
                            "type": "static_map",
                            "definitions": {
                                "address": {"definition": {"type": "ip"}},
                                "port": {"definition": {"type": "integer", "range": {"min": 1, "max": 65535}}}
                            }
                        }
                    }
                }
            },
            "bar": {
                "type": "static_map",
                "definitions": {
                    "sqlite": {
                        "definition": {
                            "type": "static_map",
                            "definitions": {
                                "recreate": {"definition": {"type": "boolean"}, "default": true},
                                "file": {"definition": {"type": "path", "file_type": "file", "access": ["write"]}}
                            }
                        }
                    }
                }
            },
            "baz": {
                "type": "static_map",
                "definitions": {
                    "logging": {
                        "definition": {
                            "type": "static_map",
                            "definitions": {
                                "level": {"definition": {"type": "log_level"}, "default": "info"},
                                "output_serialize_format": {"definition": {"type": "enum", "items": ["json", "logfmt"]}}
                            }
                        }
                    }
                }
            },
            "qux": {
                "type": "static_map",
                "definitions": {
                    "https": {
                        "definition": {
                            "type": "static_map",
                            "definitions": {
                                "insecure": {"definition": {"type": "boolean"}},
                                "follow_redirects": {"definition": {"type": "boolean"}}
                            }
                        }
                    }
                }
            }
        }
        "#;
        let rules: HashMap<_, _> = serde_json::from_str(rules_json).unwrap();

        // Here's the actual work:
        let mut configuration = Configuration::default()
            .with_url(env_url)
            .with_url(file_url);
        let apply_akippable_errors = true;
        configuration
            .try_load_parse_merge_validate(apply_akippable_errors, &rules)
            .unwrap();
        configuration
            .configuration()
            .iter()
            .for_each(|(plugin, config)| println!("{plugin}: {config:#}"));
        // Prints:
        //  foo: {"server": {"address": "127.0.0.1", "port": 8080}}
        //  baz: {"logging": {"output_serialize_format": "json", "level": "debug"}}
        //  bar: {"sqlite": {"file": "/path/to/app.db", "recreate": true}}
        //  qux: {"https": {"insecure": false, "follow_redirects": false}}
    }
}