config_manager/
__cookbook.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2022 JSRPC “Kryptonite”
3
4//! # Documentation: Cookbook
5//! 1. [Examples](#examples)
6//! 2. [Intro](#intro)
7//! 3. [Options](#options)
8//! 4. [Structure level attributes](#structure-attributes)
9//!     1. [env_prefix](#env_prefix)
10//!     2. [file](#file)
11//!     3. [clap](#clap)
12//!     4. [table](#table)
13//!     5. [default source order](#default_order)
14//! 5. [Field level attributes](#field-attributes)
15//!     1. [source](#source)
16//!         - [default](#default)
17//!         - [env](#env)
18//!         - [config](#config)
19//!         - [clap](#clap-1)
20//!         - [deserialize_with](#deserialize_with)
21//!     2. [flatten](#flatten)
22//!         - [attributes](#flatten-attributes)
23//!     3. [subcommand](#subcommand)
24//! 6. [`get_command` method](#get_command)
25//! ## Examples
26//! There are [tests](https://github.com/3xMike/config-manager/tree/main/tests)
27//! and [examples](https://github.com/3xMike/config-manager/tree/main/examples) in
28//! the crate repository to get you started
29//!
30//! ## Intro
31//! To label a structure as a config, it is required to annotate it with `#[config]`:
32//! ```
33//! use config_manager::config;
34//!
35//! #[config]
36//! struct Application {}
37//! ```
38//! **OBTAINING RESULTS**\
39//! [ConfigInit](../trait.ConfigInit.html) trait will be derived
40//! for the struct and one can invoke the initialization and obtain the result
41//! with [`<Application as ConfigInit>::parse()`](../trait.ConfigInit.html#method.parse)
42//! or [`<Application as ConfigInit>::parse_options(options)`](../trait.ConfigInit.html#tymethod.parse_options) method.
43//!  
44//! **All the sources of the value of a field must be specified explicitly.** Fields that does not
45//! have at least one source specified are not allowed.
46//! ```
47//! use config_manager::config;
48//!
49//! #[config]
50//! struct Application {
51//!     #[source(clap(long = "cli_my_field"), env = "ENV_MY_FIELD")]
52//!     my_field: i32,
53//! }
54//! ```
55//!
56//! In this example, it will be checked that `cli_my_field` is specified via the command line
57//! interface (i.e. <nobr>`./your_binary --cli_my_field=42`;</nobr>
58//! see [the clap documentation](https://docs.rs/clap/3.1.18/clap/) for more details).
59//! If `cli_my_field` is indeed specified, it will be parsed with `serde`
60//! and, if the parsing is successful, the value for `my_field` will be assigned from the result.
61//! In case of a parsing error, the error will be returned instead.
62//!
63//! If `cli_my_field` is not specified, it will be checked that the `ENV_MY_FIELD`
64//! environment variable is present. If the `ENV_MY_FIELD` environment variable is present, its
65//! value will be parsed with `serde` and, if the parsing is successful, the value for `my_field`
66//! will be assigned from the result. In case of a parsing error, the error will be returned instead.
67//!
68//! If the `ENV_MY_FIELD` environment variable is not found, an error will be returned, because
69//! this is the last source we can take the value from, and there was a failure.
70//!
71//! ### Note
72//! The order of the sources is important! The following example does **NOT** do the same thing as
73//! the previous:
74//! ```
75//! use config_manager::config;
76//!
77//! #[config]
78//! struct Application {
79//!     #[source(env = "ENV_MY_FIELD", clap(long = "cli_my_field"))]
80//!     my_field: i32,
81//! }
82//! ```
83//!
84//! In this example, the `env` source will be checked first.
85//!
86//! **NOTES**
87//! - The possible sources are: `clap`, `env`, `config`, `default` (see below)
88//! - Default value will be assigned the last (after the others were not found).
89//! - If the value is not found in any of the sources, an error will be returned
90//! - Field type must implement `serde::de::Deserialize`
91//! - All attributes except `default` must match either `attribute = literal`, or
92//! `attribute(init_from = "...valid Rust code...")`, or `attribute`. In the last case, the "key"
93//! value (the CLI argument name, the environment variable name, or the config file key name —
94//! depending on the source) will match the field name. For example, annotating `my_field` with
95//! `#[clap]` means that the value could be assigned to `my_field` by specifying
96//! `--my_field=...` via the CLI
97//! - Attribute `default` must match `default = "...valid Rust code..."` or `default`
98//! - `expression` from `default = "expression"` will be interpreted as a Rust expression (for example, `expression` could be a function call)
99//! - If the `deserialize_with` attribute is not set, values from command line,
100//! environment will be deserialized according to [hjson syntax](https://hjson.github.io/)
101//!
102//! ## Options
103//! Parsing process may be run with a set of options by using the [ConfigInit::parse_options(options)](../trait.ConfigInit.html#tymethod.parse_options).
104//! The key point here is the fact that the options take precedence over the corresponding structure attributes, that can be useful in testing and other cases.\
105//! More information can be found in the [ConfigOption](../enum.ConfigOption.html) documentation.
106//!
107//! ## Structure attributes
108//! ### `env_prefix`
109//! Prefix of the environment variables. If not specified, the prefix will not be added.
110//! Thus, the `iter` field in the example below will be searched in the environment by the `demo_iter` key.
111//! ```
112//! # use config_manager::config;
113//! #
114//! #[config(
115//!     env_prefix = "demo"
116//! )]
117//! struct AppConfig {
118//!     #[source(env)]
119//!     iter: i32,
120//! }
121//! ```
122//! **Notes**
123//! - The delimiter ('_') is placed automatically
124//! - `env_prefix = ""` will not add any prefix
125//! - `env`, `env_prefix` and similar attributes are case-insensitive. If both the `demo_iter` and
126//! `DEMO_ITER` environment variables are present, which of these two will be parsed *is not defined*
127//! - One can use `env_prefix` (without a value) to set the binary file name as a prefix
128//! **Example**
129//! ```
130//! # use config_manager::config;
131//! #
132//! #[config(env_prefix)]
133//! struct Config {
134//!     #[source(env)]
135//!     capacity: i32,
136//! }
137//! ```
138//! In the example above, the `capacity` field will be searched in the environment
139//! by the "*bin*_capacity" key, where `bin` is the name of the executable file.
140//!
141//! ### `file`
142//! Description of the configuration file. Has the following nested attributes:
143//! - `format`: `toml`/`json`/`yaml`/`ron`/`json5`
144//! - `env`: environment key containing path to the configuration file (case-insensitive)
145//! - `clap`: clap attributes of the argument, responsible for the path to the configuration file\
146//! **Note:** in this case, clap attribute must have the nested `long` attribute (`clap(long = "...")`)
147//! - `default`: default configuration file path
148//! - `optional`:  boolean attribute: should the macro panic (`false`) or not (`true`)\
149//! **Note:** It is allowed to specify multiple files: all of them will be merged.
150//! If there is a collision (the values of a particular key have been specified in two or more files),
151//! the value will be assigned from the file that has been described later (in the attribute list).
152//! **Example**
153//! ```
154//! # use config_manager::config;
155//! #
156//! #[config(
157//!     env_prefix = "",
158//!     file(format = "toml", env = "demo_config")
159//! )]
160//! struct AppConfig {
161//! #[source(clap(long), env, default = 5)]
162//!     iter: i32,
163//! }
164//! ```
165//! In this case, the initialization order for the `iter` field is:
166//! - command line argument `--iter`
167//! - environment variable `iter`
168//! - variable `iter` from configuration file with path set by the `demo_config` environment variable
169//! - default value (`5`)
170//!
171//! ### `clap`
172//! Clap app attributes: `name`, `version`, `author`, `about`, `long_about`
173//!
174//! ### `table`
175//! Table of the configuration files to find fields of the structure.
176//!
177//! **Example**
178//! ```
179//! # use config_manager::config;
180//! #
181//! #[config(file(format = "toml", default = "./config.toml"), table = "input.data")]
182//! struct Config {
183//!     #[source(config)]
184//!     frames: i32,
185//! }
186//! ```
187//! Field `frames` will be searched in the "input.data" table of the configuration file "config.toml".
188//!
189//! ### `default_order`
190//! The default order of any field that wasn't annotated with any of `source`,`flatten` or `subcommand`.\
191//! `clap`, `env`, `config` and `default` are all possible parameters.
192//! Each attribute will be applied to each unannotated field in a "short" form
193//! (i.e., form without value; for example, `#[source(default)]` means that
194//! `Default::default()` will be used as a default value. See the [source](#source) section for more information).
195//! **Example**
196//! ```
197//! # use config_manager::config;
198//! #
199//! #[config(default_order(env, clap, default))]
200//! struct Config {
201//!     rotation: f32,
202//! }
203//! ```
204//! It will be checked that the `ROTATION` environment variable is set; if not, the `--rotation` command line argument will be checked,
205//! and, lastly, the `Default::default()` will be assigned.
206//! **Note:** If this attribute isn't set, the default order is:
207//! 1. command line
208//! 2. environment variables
209//! 3. configuration files
210//!
211//! ## Field attributes
212//! Only fields can be annotated with the following attributes and only one of them can be assigned to a field.
213//!
214//! **Note:** if a field is not annotated with any of the following attributes,
215//! it will be parsed using the default source order (see the section above).
216//!
217//! ### Source
218//! If a field is annotated with the `source` attribute, at least one of the following nested attributes must be present.
219//!
220//! #### `default`
221//! Numeric literal or valid Rust code. \
222//! If the attribute is set without a value (`#[source(default)]`),
223//! the default value is [`Default::default()`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default).
224//!
225//! **Example**
226//! ```
227//! # use config_manager::config;
228//! #
229//! #[config]
230//! struct AppConfig {
231//!     #[source(default = "Vec::new()")]
232//!     buf: Vec<String>,
233//!     #[source(default)]
234//!     opt: Option<String>
235//!     // Option::<String>::default() will be assigned (None)
236//! }
237//! ```
238//!
239//! #### `env`
240//! The name of the environment variable from which the value is to be set.
241//! `env_prefix` (see above) is ignored if present with a value (`#[source(env = "...")]`).  The case is ignored. \
242//! If the attribute is set without value, the name of the environment variable to be set is `env_prefix + field_name`.
243//!
244//! #### `config`
245//! Name of the configuration file field to set the value from. It can contain dots: in this case
246//! the name will be parsed as the path of the field.\
247//! If the attribute is set without a value (`#[source(config)]`), the field name is the name of the configuration file field to be set.
248//! **Example**
249//! ```
250//! # use config_manager::config;
251//! #
252//! #[config(file(format = "toml", default = "./config.toml"), table = "input.data")]
253//! struct Config {
254//!     #[source(config = "images.frame_rate")]
255//!     rate: i32,
256//! }
257//! ```
258//! Field `rate` will be searched in the "input.data.images" table of the "config.toml"
259//! configuration file by the `frame_rate` key.
260//!
261//! #### `clap`
262//! Clap-crate attributes. Available nested attributes: `help`, `long_help`, `short`, `long`,
263//! `flatten`, `subcommand`.
264//! **Note:** the default `long` and `short` values (`#[clap(long)]` and `#[clap(short)]`) is the field name and it's first letter respectively. \
265//! `#[source(clap)]` is equivalent to `#[source(clap(long))]` \
266//!
267//! In addition, the following attribute can be used.
268//! #### `deserialize_with`
269//! Custom deserialization of the field. The deserialization function must have the signature
270//! ```ignore
271//! fn fn_name(s: &str) -> Result<FieldType, String>
272//! ```
273//!
274//! **Example**
275//! ```
276//! # use config_manager::config;
277//! use std::time::Duration;
278//!
279//! #[config]
280//! struct MethodConfig {
281//!     #[source(clap(long), deserialize_with = "deser_duration")]
282//!     a: Duration,
283//! }
284//!
285//! fn deser_duration(dur: &str) -> Result<Duration, String> {
286//!     match dur.parse::<u64>() {
287//!         Ok(dur) => Ok(Duration::from_millis(dur)),
288//!         Err(err) => Err(err.to_string()),
289//!     }
290//! }
291//! ```
292//!
293//! ### Flatten
294//! If a field is annotated with the `flatten` attribute, it will be parsed as a nested structure and its fields will be initiated
295//! like fields of the primary config. In this case, the field's type must implement `config_manager::Flatten`
296//! (it is highly discouraged to implement this trait manually, use derive macro: `#[derive(Flatten)]`) and `serde::Deserialize`
297//!
298//! **Example**
299//! ```
300//! use config_manager::{config, Flatten};
301//! # use serde::Deserialize;
302//! #
303//! #[config]
304//! struct PrimalConfig {
305//!     #[flatten]
306//!     child: NestedConfig,
307//! }
308//!
309//! #[derive(Deserialize, Flatten)]
310//! struct NestedConfig {
311//!     #[source(env = "recharge")]
312//!     recharge_time: f32,
313//!     #[source(default = 0.0)]
314//!     capacity: f32,
315//! }
316//! ```
317//! **Notes:**
318//! - Nested configs can also contain `flatten` fields
319//! - `env_prefix` will be inherited from the initial struct
320//!
321//! #### Flatten attributes
322//! Flatten struct may have the following helper attributes: `table`, `flatten`, `source` (they work the same way as the described above ones).
323//! ### Subcommand
324//! If a field is annotated with the `subcommand` attribute, it will be taken as a `clap` subcommand
325//! (see [clap documentation](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#subcommands) for more info).
326//! The field's type must implement `clap::Subcommand` and `serde::Deserialize`.
327//!
328//! **Example**
329//! ```
330//! # use config_manager::config;
331//! # use serde::Deserialize;
332//! # use clap;
333//! #
334//! #[config]
335//! struct Cargo {
336//!     #[subcommand]
337//!     sub: CargoCommands,
338//! }
339//!
340//! #[derive(Deserialize, clap::Subcommand)]
341//! enum CargoCommands {
342//!     #[clap(about = "Compile the current package")]
343//!     Build {
344//!     // ...
345//!     },
346//!     #[clap(about = "Analyze the current package and report errors, but don't build object files")]
347//!     Check {
348//!     // ...
349//!     },
350//!     #[clap(about = "Build this package's and its dependencies' documentation")]
351//!     Doc,
352//!     #[clap(about = "Create a new cargo package")]
353//!     New,
354//!     // ...
355//! }
356//! ```
357//! **Notes:**
358//! - Value for the `subcommand` enumeration will be searched only in command line, so the `source` and the `flatten` attributes are forbidden
359//! (flatten `subcommand` attribute is allowed due to clap documentation).
360//! - Multiple `subcommand` fields are forbidden.
361//! - `subcommand` field in nested(`flatten`) structures are forbidden.
362//! - `subcommand` field can be optional (`Option<T>`, `T: clap::Subcommand + serde::Deserialize`),
363//! so if no subcommand is found in the command line, the `None` will be assigned.
364//!
365//! ## get_command
366//! [ConfigInit](../trait.ConfigInit.html) trait has the [get_command](../trait.ConfigInit.html#tymethod.get_command)
367//! method that builds [Command](https://docs.rs/clap/latest/clap/struct.Command.html) that can initialize the structure. \
368//! By using this method along with the [ClapSource::Matches](config_manager/enum.ClapSource.html#variant.Matches) option,
369//! one can initialize the structure as a subcommand, so settings of the application and the configuration can be divided, like:
370//! ```console
371//! binary [APPLICATION'S SETTINGS] configuration [CONFIGURATION'S SETTINGS]
372//! ```
373//!
374//! **Example**
375//! ```
376//! # use std::collections::HashSet;
377//! use config_manager::*;
378//! use clap::*;
379//!
380//! #[config(clap(name = "configuration", version, author))]
381//! struct Config {
382//! #[source(clap(long, short))]
383//!     a: i32,
384//! }
385//!
386//! fn init_from_app() -> Option<Config> {
387//!     let app = Command::new("binary")
388//!         .arg(Arg::new("some_field").long("field"))
389//!         .subcommand(Config::get_command())
390//!         .get_matches();
391//!
392//!     if let Some(subcommand) = app.subcommand_matches(Config::get_command().get_name()) {
393//!         let opts = ConfigOption::ExplicitSource(Source::Clap(ClapSource::Matches(subcommand.clone())));
394//!         Some(Config::parse_options(HashSet::from([opts])).unwrap())
395//!     } else {
396//!         None
397//!     }
398//! }
399//! ```