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//!
56//! #[derive(Debug)]
57//! #[config(
58//! clap(version, author),
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//! #[source(clap(long, short))]
69//! a: i32,
70//! #[source(
71//! env(init_from = "&format!(\"b{}\", SUFFIX)"),
72//! default = "\"abc\".to_string()"
73//! )]
74//! b: String,
75//! #[source(config = "bpm")]
76//! c: i32,
77//! #[source(default = "HashMap::new()")]
78//! d: HashMap<i32, String>,
79//! }
80//!
81//! fn main() {
82//! dbg!(MethodConfig::parse().unwrap());
83//! }
84//! ```
85//! Run in [the repository](https://github.com/3xMike/config-manager)
86//! ```console
87//! cargo run --package examples --bin demo -- --config="examples/config.toml" --a=5
88//! ```
89//! Result must be:
90//! ```console
91//! [examples/src/demo.rs:34] &*CFG = MethodConfig {
92//! a: 5,
93//! b: "qwerty",
94//! c: 165,
95//! d: {},
96//! }
97//! ```
98
99use std::collections::HashMap;
100use std::collections::HashSet;
101use std::fmt;
102
103pub use config_manager_proc::config;
104pub use config_manager_proc::Flatten;
105pub mod __cookbook;
106#[doc(hidden)]
107pub mod __private;
108#[doc(hidden)]
109mod utils;
110
111/// Runtime initializing error.
112#[derive(Debug)]
113pub enum Error {
114 MissingArgument(String),
115 FailedParse(String),
116 ExternalError(String),
117}
118
119impl fmt::Display for Error {
120 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121 match self {
122 Error::MissingArgument(msg) | Error::FailedParse(msg) | Error::ExternalError(msg) => {
123 write!(f, "{}", msg)
124 }
125 }
126 }
127}
128
129impl std::error::Error for Error {}
130/// Config trait that constructs an instance of itself from
131/// environment, command line and configuration files. \
132///
133/// Don't implement the trait manually,
134/// invoking `#[config]` is the only correct way to derive this trait.
135pub trait ConfigInit {
136 /// Takes all the environment and tries to build an instance according to the structure attributes.
137 fn parse() -> Result<Self, Error>
138 where
139 Self: Sized,
140 {
141 Self::parse_options(HashSet::new())
142 }
143
144 /// Takes all the environment and tries to build an instance according to the options and the structure attributes.
145 fn parse_options(options: ConfigOptions) -> Result<Self, Error>
146 where
147 Self: Sized;
148
149 /// Build `clap::Command` that can initialize the annotated struct.
150 fn get_command() -> clap::Command;
151}
152
153/// Set of rules to build an instance of the annotated by `#[config]` structure.
154pub type ConfigOptions = HashSet<ConfigOption>;
155/// Allowed formats for the configuration files.
156///
157/// **Note:** `Ini` format is not supported.
158pub type FileFormat = config::FileFormat;
159
160/// Settings to build an instance of a struct, implementing `ConfigInit`.
161///
162/// **Note** Each option takes precedence over the corresponding structure attribute (see [cookbook](__cookbook/index.html) for more information).
163#[derive(Debug, Clone)]
164pub enum ConfigOption {
165 /// Prefix of the environment variables.
166 EnvPrefix(String),
167 /// Replacement of the usual source.
168 ExplicitSource(Source),
169}
170
171/// Replacement of the usual source to find values for the fields.
172#[derive(Debug, Clone)]
173pub enum Source {
174 /// Configuration files.
175 ///
176 /// **Note:** It is allowed to specify multiple files: all of them will be merged.
177 /// If there is a collision (the values of a particular key have been specified in two or more files),
178 /// the value will be assigned from the file that has been described later.
179 ConfigFiles(Vec<FileOptions>),
180 /// Command line source.
181 Clap(ClapSource),
182 /// Map that replaces the enviromnent (fields, annotated with #[source(env)] will be searched in this map).
183 ///
184 /// Can be useful in testing.
185 Env(HashMap<String, String>),
186}
187
188/// Replacement of the command line source.
189#[derive(Debug, Clone)]
190pub enum ClapSource {
191 /// Same as ClapSource::Args(Vec::new()).
192 None,
193 /// Values of the command line source will be got from the passed arguments (like they were the command line arguments).
194 ///
195 /// Can be useful in testing.
196 Args(Vec<String>),
197 /// Values of the command line source will be got from the passed ArgMatches.
198 ///
199 /// Can be useful if the configuration is a subcommand of the main programm.
200 Matches(::clap::ArgMatches),
201}
202
203/// Description of the configuration file.
204#[derive(Debug, Clone)]
205pub struct FileOptions {
206 /// File format.
207 pub format: FileFormat,
208 /// Path to the file.
209 pub path: String,
210}