config_manager/
lib.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2022 JSRPC “Kryptonite”
3
4//! > **Crate to build config from environment, command line and files**
5//! # Motivation
6//! Non-runtime data generally comes to a project from
7//! command line, environment and configuration files.\
8//! Sometimes it comes from each of the sources simultaneously,
9//! so all of them must be handled.\
10//! None of the popular crates (including [clap](https://docs.rs/clap/latest/clap/) and [config](https://docs.rs/config/latest/config/))
11//! can't handle all 3 together, so this crate has been created to solve this problem.
12//!
13//! # Basis
14//! The Core of the crate is an attribute-macro [config](attr.config.html). \
15//! Annotate structure with this macro and a field of it with the `source` attribute,
16//! so the field will be searched in one of the provided sources. The sources can be provided by using the following nested `source` attributes: \
17//! 1. `clap`: command line argument
18//! 2. `env`: environment variable
19//! 3. `config`: configuration file key
20//! 4. `default`: default value
21//!
22//! **Example**
23//! ```
24//! use config_manager::config;
25//!
26//! #[config]
27//! struct ApplicationConfig {
28//!     #[source(clap(long, short = 'p'), env = "APP_MODEL_PATH", config)]
29//!     model_path: String,
30//!     #[source(env, config, default = 0)]
31//!     prediction_delay: u64,
32//! }
33//! ```
34//! In the example above, to set the value of the `model_path` field, a user may provide:
35//! - command line argument `--model_path`
36//! - environment variable named `model_path`
37//! - configuration file containing field `model_path`
38//!
39//! If the value is found in multiple provided sources, the value will be assigned according to the provided order
40//! (the order for the `model_path` field is `clap -> env -> config` and `env -> config -> default` for the `prediction_delay`). \
41//! If none of them (including the default value) is found, the program returns error `MissingArgument`.
42//!
43//! **Note:** the default value is always assigned last.
44//!
45//! # Attributes documentation
46//! For further understanding of project syntax and features, it is recommended to visit [Cookbook](__cookbook).
47//!
48//! # Complex example
49//! ```no_run
50//! use std::collections::HashMap;
51//!
52//! use config_manager::{config, ConfigInit};
53//!
54//! const SUFFIX: &str = "_env";
55//! /// This doc will be included to CLI long_about.
56//! #[derive(Debug)]
57//! #[config(
58//!     clap(version, author, long_about),
59//!     env_prefix = "demo",
60//!     file(
61//!         format = "toml",
62//!         clap(long = "config", short = 'c', help = "path to configuration file"),
63//!         env = "demo_config",
64//!         default = "./config.toml"
65//!     )
66//! )]
67//! struct MethodConfig {
68//!     /// This doc will be included to CLI help.
69//!     #[source(clap(long, short, help))]
70//!     a: i32,
71//!     #[source(
72//!         env(init_from = &format!("b{}", SUFFIX)),
73//!         default = "abc"
74//!     )]
75//!     b: String,
76//!     #[source(config = "bpm")]
77//!     c: i32,
78//!     #[source(default = HashMap::new())]
79//!     d: HashMap<i32, String>,
80//! }
81//!
82//! fn main() {
83//!     dbg!(MethodConfig::parse().unwrap());
84//! }
85//! ```
86//! Run in [the repository](https://github.com/3xMike/config-manager)
87//! ```console
88//! cargo run --package examples --bin demo -- --config="examples/config.toml" --a=5
89//! ```
90//! Result must be:
91//! ```console
92//! [examples/src/demo.rs:34] &*CFG = MethodConfig {
93//!     a: 5,
94//!     b: "qwerty",
95//!     c: 165,
96//!     d: {},
97//! }
98//! ```
99
100use std::collections::HashMap;
101use std::collections::HashSet;
102use std::fmt;
103
104pub use config_manager_proc::config;
105pub use config_manager_proc::Flatten;
106pub mod __cookbook;
107#[doc(hidden)]
108pub mod __private;
109#[doc(hidden)]
110mod utils;
111
112/// Runtime initializing error.
113#[derive(Debug)]
114pub enum Error {
115    MissingArgument(String),
116    FailedParse(String),
117    ExternalError(String),
118}
119
120impl fmt::Display for Error {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        match self {
123            Error::MissingArgument(msg) | Error::FailedParse(msg) | Error::ExternalError(msg) => {
124                write!(f, "{}", msg)
125            }
126        }
127    }
128}
129
130impl std::error::Error for Error {}
131/// Config trait that constructs an instance of itself from
132/// environment, command line and configuration files. \
133///
134/// Don't implement the trait manually,
135/// invoking `#[config]` is the only correct way to derive this trait.
136pub trait ConfigInit {
137    /// Takes all the environment and tries to build an instance according to the structure attributes.
138    fn parse() -> Result<Self, Error>
139    where
140        Self: Sized,
141    {
142        Self::parse_options(HashSet::new())
143    }
144
145    /// Takes all the environment and tries to build an instance according to the options and the structure attributes.
146    fn parse_options(options: ConfigOptions) -> Result<Self, Error>
147    where
148        Self: Sized;
149
150    /// Build `clap::Command` that can initialize the annotated struct.
151    fn get_command() -> clap::Command;
152}
153
154/// Set of rules to build an instance of the annotated by `#[config]` structure.
155pub type ConfigOptions = HashSet<ConfigOption>;
156/// Allowed formats for the configuration files.
157///
158/// **Note:** `Ini` format is not supported.
159pub type FileFormat = config::FileFormat;
160
161/// Settings to build an instance of a struct, implementing `ConfigInit`.
162///
163/// **Note** Each option takes precedence over the corresponding structure attribute (see [cookbook](__cookbook/index.html) for more information).
164#[derive(Debug, Clone)]
165pub enum ConfigOption {
166    /// Prefix of the environment variables.
167    EnvPrefix(String),
168    /// Replacement of the usual source.
169    ExplicitSource(Source),
170}
171
172/// Replacement of the usual source to find values for the fields.
173#[derive(Debug, Clone)]
174pub enum Source {
175    /// Configuration files.
176    ///
177    /// **Note:** It is allowed to specify multiple files: all of them will be merged.
178    /// If there is a collision (the values of a particular key have been specified in two or more files),
179    /// the value will be assigned from the file that has been described later.
180    ConfigFiles(Vec<FileOptions>),
181    /// Command line source.
182    Clap(ClapSource),
183    /// Map that replaces the enviromnent (fields, annotated with #[source(env)] will be searched in this map).
184    ///
185    /// Can be useful in testing.
186    Env(HashMap<String, String>),
187}
188
189/// Replacement of the command line source.
190#[derive(Debug, Clone)]
191pub enum ClapSource {
192    /// Same as ClapSource::Args(Vec::new()).
193    None,
194    /// Values of the command line source will be got from the passed arguments (like they were the command line arguments).
195    ///
196    /// Can be useful in testing.
197    Args(Vec<String>),
198    /// Values of the command line source will be got from the passed ArgMatches.
199    ///
200    /// Can be useful if the configuration is a subcommand of the main programm.
201    Matches(::clap::ArgMatches),
202}
203
204/// Description of the configuration file.
205#[derive(Debug, Clone)]
206pub struct FileOptions {
207    /// File format.
208    pub format: FileFormat,
209    /// Path to the file.
210    pub path: String,
211}