diff --git a/CHANGELOG.md b/CHANGELOG.md index b89a18b..7b27fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,5 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +## [0.1.1](https://github.com/Kryptonite-RU/config-manager-rs/releases/tag/0.1.0) - 2023-02-27 +### Added +- If a field is not annotated, default source order will be assigned +### Changed +- The default behavior of `env_prefix` is no prefix instead of a binary name now ## [0.1.0](https://github.com/Kryptonite-RU/config-manager-rs/releases/tag/0.1.0) - 2022-12-27 Initial tag \ No newline at end of file diff --git a/config-manager-proc/src/lib.rs b/config-manager-proc/src/lib.rs index 599aa44..4085e7c 100644 --- a/config-manager-proc/src/lib.rs +++ b/config-manager-proc/src/lib.rs @@ -12,7 +12,7 @@ use quote::{quote, ToTokens}; use syn::{parse::Parser, punctuated::Punctuated, *}; use generator::*; -use utils::{config::*, field::*, field_to_tokens, parser::*, top_level::*}; +use utils::{config::*, field::*, parser::*, top_level::*}; /// Macro generating an implementation of the `ConfigInit` trait /// or constructing global variable. \ @@ -48,6 +48,7 @@ pub fn config(attrs: TokenStream0, input: TokenStream0) -> TokenStream0 { global_name, file, table, + default_order, __debug_cmd_input__ ) )] @@ -62,6 +63,7 @@ pub fn generate_config(input: TokenStream0) -> TokenStream0 { configs, debug_cmd_input, table_name, + default_order, } = AppTopLevelInfo::extract(&input.attrs); let class: DataStruct = match input.data { @@ -85,14 +87,8 @@ pub fn generate_config(input: TokenStream0) -> TokenStream0 { process_flatten_field(field) } else if field_is_subcommand(&field) { process_subcommand_field(field, &debug_cmd_input) - } else if field_is_source(&field) { - process_field(field, &table_name) } else { - panic!( - "Error: each field must be annotated with one of the following: \ - source/flatten/subcommand (field's name: \"{}\")", - field_to_tokens(&field) - ) + process_field(field, &table_name, &default_order) }; ((name, initialization), clap_field) }) @@ -112,11 +108,12 @@ pub fn generate_config(input: TokenStream0) -> TokenStream0 { /// Annotated with this macro structure can be used /// as a flatten argument in the [config](attr.config.html) macro. -#[proc_macro_derive(Flatten, attributes(source, flatten, subcommand, table))] +#[proc_macro_derive(Flatten, attributes(source, flatten, subcommand, table, default_order))] pub fn generate_flatten(input: TokenStream0) -> TokenStream0 { let input = parse_macro_input!(input as DeriveInput); let table_name = extract_table_name(&input.attrs); + let default_order = extract_source_order(&input.attrs); let class_ident = input.ident; let class: DataStruct = match input.data { @@ -140,14 +137,8 @@ pub fn generate_flatten(input: TokenStream0) -> TokenStream0 { process_flatten_field(field) } else if field_is_subcommand(&field) { panic!("subcommands are forbidden in the nested structures") - } else if field_is_source(&field) { - process_field(field, &table_name) } else { - panic!( - "Error: each field must be annotated with one of the following: \ - source/flatten (field's name: \"{}\")", - field_to_tokens(&field) - ) + process_field(field, &table_name, &default_order) }; ((name, initialization), clap_field) }) diff --git a/config-manager-proc/src/utils.rs b/config-manager-proc/src/utils.rs index 07b038c..74dc9e4 100644 --- a/config-manager-proc/src/utils.rs +++ b/config-manager-proc/src/utils.rs @@ -9,14 +9,6 @@ pub(crate) mod field; pub(crate) mod parser; pub(crate) mod top_level; -pub(crate) fn field_to_tokens(field: &Field) -> TokenStream { - field - .ident - .clone() - .expect("Unnamed fields are forbidden") - .to_token_stream() -} - /// Formated string to TokenStream \ /// Same as ```TokenStream::from_str(&format!(...)).unwrap()``` macro_rules! format_to_tokens { diff --git a/config-manager-proc/src/utils/attributes.rs b/config-manager-proc/src/utils/attributes.rs index df8a153..008f360 100644 --- a/config-manager-proc/src/utils/attributes.rs +++ b/config-manager-proc/src/utils/attributes.rs @@ -13,6 +13,7 @@ pub(crate) const SOURCE_KEY: &str = "source"; pub(crate) const CONFIG_FILE_KEY: &str = "file"; pub(crate) const DEBUG_INPUT_KEY: &str = "__debug_cmd_input__"; pub(crate) const TABLE_NAME_KEY: &str = "table"; +pub(crate) const SOURCE_ORDER_KEY: &str = "default_order"; pub(crate) const FLATTEN: &str = "flatten"; pub(crate) const SUBCOMMAND: &str = "subcommand"; diff --git a/config-manager-proc/src/utils/config.rs b/config-manager-proc/src/utils/config.rs index 8d98e9f..78303ff 100644 --- a/config-manager-proc/src/utils/config.rs +++ b/config-manager-proc/src/utils/config.rs @@ -8,8 +8,8 @@ use strum::IntoEnumIterator; use super::attributes::*; use crate::*; -fn str_to_config_format_repr>(s: T) -> String { - match s.as_ref() { +fn str_to_config_format_repr(s: &str) -> String { + match s { "json" | "json5" | "toml" | "yaml" | "ron" => { let capitalize_first = |s: &str| -> String { let mut chars = s.chars(); @@ -17,11 +17,11 @@ fn str_to_config_format_repr>(s: T) -> String { first_char.to_uppercase().to_string() + &chars.collect::() }; - let accepted_format = capitalize_first(s.as_ref()); + let accepted_format = capitalize_first(s); let pref = "::config_manager::__private::config::FileFormat::".to_string(); pref + &accepted_format } - _ => panic!("{} format is not supported", s.as_ref()), + _ => panic!("{} format is not supported", s), } } @@ -117,7 +117,7 @@ fn handle_file_attribute( .skip(1) .take(format_atr.len() - 2) .collect(); - file_format = Some(str_to_config_format_repr(drop_fst_and_lst)) + file_format = Some(str_to_config_format_repr(&drop_fst_and_lst)) } } } diff --git a/config-manager-proc/src/utils/field.rs b/config-manager-proc/src/utils/field.rs index 629b29e..03b6aab 100644 --- a/config-manager-proc/src/utils/field.rs +++ b/config-manager-proc/src/utils/field.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2022 JSRPC “Kryptonite” -mod utils; +pub(crate) mod utils; use super::{attributes::*, format_to_tokens}; use crate::*; @@ -29,16 +29,13 @@ pub(crate) struct ProcessFieldResult { pub(crate) initialization: TokenStream, } -pub(crate) fn field_is_source(field: &Field) -> bool { - field - .attrs - .iter() - .any(|attr| compare_attribute_name(attr, SOURCE_KEY)) -} - -pub(crate) fn process_field(field: Field, table_name: &Option) -> ProcessFieldResult { +pub(crate) fn process_field( + field: Field, + table_name: &Option, + default_order: &Option, +) -> ProcessFieldResult { let field_name = field.ident.clone().expect("Unnamed fields are forbidden"); - if number_of_crate_attribute(&field) != 1 { + if number_of_crate_attribute(&field) > 1 { panic!( "Error: source attribute must be the only attribute of the field (field's name: \ \"{}\")", @@ -46,7 +43,16 @@ pub(crate) fn process_field(field: Field, table_name: &Option) -> Proces ); } - let attributes_order = extract_attributes(field, table_name.clone()); + let attributes_order = extract_attributes(field, table_name) + .or_else(|| default_order.clone()) + .unwrap_or_else(|| ExtractedAttributes { + variables: vec![ + FieldAttribute::Clap(std::default::Default::default()), + FieldAttribute::Env(std::default::Default::default()), + FieldAttribute::Config(std::default::Default::default()), + ], + ..std::default::Default::default() + }); ProcessFieldResult { initialization: attributes_order.gen_init(&field_name.to_string()), diff --git a/config-manager-proc/src/utils/field/utils.rs b/config-manager-proc/src/utils/field/utils.rs index 1c183c5..136b9ae 100644 --- a/config-manager-proc/src/utils/field/utils.rs +++ b/config-manager-proc/src/utils/field/utils.rs @@ -49,11 +49,11 @@ impl ToTokens for NormalClapFieldInfo { } } -#[derive(Default)] -pub(super) struct ExtractedAttributes { - variables: Vec, - default: Option, - deserializer: Option, +#[derive(Default, Clone)] +pub(crate) struct ExtractedAttributes { + pub(crate) variables: Vec, + pub(crate) default: Option, + pub(crate) deserializer: Option, } impl ExtractedAttributes { @@ -144,7 +144,8 @@ impl ExtractedAttributes { } } -pub(super) enum FieldAttribute { +#[derive(Clone)] +pub(crate) enum FieldAttribute { Clap(ClapFieldParseResult), Env(Env), Config(Config), @@ -193,7 +194,8 @@ impl Display for FieldAttribute { } } -pub(super) struct Env { +#[derive(Default, Clone)] +pub(crate) struct Env { pub(super) inner: Option, } @@ -230,7 +232,8 @@ impl Env { } } -pub(super) struct Config { +#[derive(Default, Clone)] +pub(crate) struct Config { pub(super) key: Option, pub(super) table: Option, } @@ -249,101 +252,118 @@ impl Config { } } -pub(super) struct Default { +#[derive(Default, Clone)] +pub(crate) struct Default { pub(super) inner: Option, } -pub(super) fn extract_attributes(field: Field, table_name: Option) -> ExtractedAttributes { +pub(super) fn extract_attributes( + field: Field, + table_name: &Option, +) -> Option { let field_name = field.ident.expect("Unnamed fields are forbidden"); let mut res = ExtractedAttributes::default(); - if let Some(atr) = field + field .attrs .iter() .find(|a| compare_attribute_name(a, SOURCE_KEY)) - { - match atr.parse_meta() { - Err(err) => panic!("Can't parse attribute as meta: {err}"), - Ok(meta) => match meta { - Meta::List(MetaList { nested: args, .. }) => { - for arg in args { - match arg { - NestedMeta::Lit(lit) => { - panic!("source attribute ({:#?}) can't be a literal", lit) - } - NestedMeta::Meta(arg) => match path_to_string(arg.path()).as_str() { - CLAP_KEY => match arg { - Meta::Path(_) => res.variables.push(FieldAttribute::Clap( - ClapFieldParseResult::default(), - )), - Meta::List(clap_metalist) => { - res.variables.push(FieldAttribute::Clap( - parse_clap_field_attribute(&clap_metalist), - )); - } - _ => { - panic!("clap attribute must match #[clap(...)] or #[clap]") - } - }, - DEFAULT => { - if res.default.is_some() { - panic!("Default can be assigned only once per field") - } - res.default = Some(Default { - inner: match_literal_or_init_from( - &arg, - AcceptedLiterals::AnyLiteral, - ) - .map(|init| match init { - InitFrom::Fn(func) => format!("{{{func}}}"), - InitFrom::Literal(lit) => match lit { - Lit::Str(str) => str.value(), - lit => lit.to_token_stream().to_string(), - }, - }), - }) + .map(|atr| { + match atr.parse_meta() { + Err(err) => panic!("Can't parse attribute as meta: {err}"), + Ok(meta) => match meta { + Meta::List(MetaList { nested: args, .. }) => { + for arg in args { + match arg { + NestedMeta::Lit(lit) => { + panic!("source attribute ({:#?}) can't be a literal", lit) } - ENV_KEY => res.variables.push(FieldAttribute::Env(Env { - inner: match_literal_or_init_from( - &arg, - AcceptedLiterals::StringOnly, - ) - .as_ref() - .map(InitFrom::as_string), - })), - CONFIG_KEY => res.variables.push(FieldAttribute::Config(Config { - key: match_literal_or_init_from( - &arg, - AcceptedLiterals::StringOnly, - ) - .as_ref() - .map(InitFrom::as_string), - table: table_name.clone(), - })), - DESERIALIZER => { - if res.deserializer.is_some() { - panic!( - "Deserialize_with can be assigned only once per field" - ) + NestedMeta::Meta(arg) => { + match path_to_string(arg.path()).as_str() { + CLAP_KEY => match arg { + Meta::Path(_) => { + res.variables.push(FieldAttribute::Clap( + ClapFieldParseResult::default(), + )) + } + Meta::List(clap_metalist) => { + res.variables.push(FieldAttribute::Clap( + parse_clap_field_attribute(&clap_metalist), + )); + } + _ => { + panic!( + "clap attribute must match #[clap(...)] or \ + #[clap]" + ) + } + }, + DEFAULT => { + if res.default.is_some() { + panic!( + "Default can be assigned only once per field" + ) + } + res.default = Some(Default { + inner: match_literal_or_init_from( + &arg, + AcceptedLiterals::AnyLiteral, + ) + .map(|init| match init { + InitFrom::Fn(func) => format!("{{{func}}}"), + InitFrom::Literal(lit) => match lit { + Lit::Str(str) => str.value(), + lit => lit.to_token_stream().to_string(), + }, + }), + }) + } + ENV_KEY => res.variables.push(FieldAttribute::Env(Env { + inner: match_literal_or_init_from( + &arg, + AcceptedLiterals::StringOnly, + ) + .as_ref() + .map(InitFrom::as_string), + })), + CONFIG_KEY => { + res.variables.push(FieldAttribute::Config(Config { + key: match_literal_or_init_from( + &arg, + AcceptedLiterals::StringOnly, + ) + .as_ref() + .map(InitFrom::as_string), + table: table_name.clone(), + })) + } + DESERIALIZER => { + if res.deserializer.is_some() { + panic!( + "Deserialize_with can be assigned only once \ + per field" + ) + } + res.deserializer = match_literal_or_init_from( + &arg, + AcceptedLiterals::StringOnly, + ) + .as_ref() + .map(InitFrom::as_string); + } + other => panic!( + "Unknown source attribute {other} of the field \ + {field_name}" + ), } - res.deserializer = match_literal_or_init_from( - &arg, - AcceptedLiterals::StringOnly, - ) - .as_ref() - .map(InitFrom::as_string); } - other => panic!( - "Unknown source attribute {other} of the field {field_name}" - ), - }, + } } } - } - _ => panic!("source attribute must match #[source(...)]"), - }, - } - } + _ => panic!("source attribute must match #[source(...)]"), + }, + } - res + res + }) } diff --git a/config-manager-proc/src/utils/parser/clap.rs b/config-manager-proc/src/utils/parser/clap.rs index 1adf262..9a40be8 100644 --- a/config-manager-proc/src/utils/parser/clap.rs +++ b/config-manager-proc/src/utils/parser/clap.rs @@ -4,7 +4,7 @@ use super::{super::attributes::*, *}; use crate::*; -#[derive(Default)] +#[derive(Default, Clone)] pub(crate) enum ClapOption { #[default] None, @@ -14,7 +14,7 @@ pub(crate) enum ClapOption { type MaybeString = ClapOption; -#[derive(Default)] +#[derive(Default, Clone)] pub(crate) struct ClapFieldParseResult { pub(crate) long: MaybeString, pub(crate) short: MaybeString, diff --git a/config-manager-proc/src/utils/parser/utils.rs b/config-manager-proc/src/utils/parser/utils.rs index 48cf803..3859bd2 100644 --- a/config-manager-proc/src/utils/parser/utils.rs +++ b/config-manager-proc/src/utils/parser/utils.rs @@ -50,10 +50,7 @@ pub(crate) fn path_to_string(path: &Path) -> String { .to_string() } -pub(crate) fn compare_attribute_name + PartialEq>( - a: &Attribute, - name: S, -) -> bool { +pub(crate) fn compare_attribute_name(a: &Attribute, name: &str) -> bool { name == path_to_string(&a.path) } diff --git a/config-manager-proc/src/utils/top_level.rs b/config-manager-proc/src/utils/top_level.rs index 48e686a..4f7c8dc 100644 --- a/config-manager-proc/src/utils/top_level.rs +++ b/config-manager-proc/src/utils/top_level.rs @@ -2,6 +2,7 @@ // Copyright (c) 2022 JSRPC “Kryptonite” use super::{attributes::*, format_to_tokens}; +use crate::utils::field::utils::{ExtractedAttributes, FieldAttribute}; use crate::*; pub(crate) struct AppTopLevelInfo { @@ -10,6 +11,7 @@ pub(crate) struct AppTopLevelInfo { pub(crate) configs: ConfigFilesInfo, pub(crate) debug_cmd_input: Option, pub(crate) table_name: Option, + pub(crate) default_order: Option, } impl AppTopLevelInfo { @@ -20,6 +22,7 @@ impl AppTopLevelInfo { configs: extract_configs_info(class_attrs), debug_cmd_input: extract_debug_cmd_input(class_attrs), table_name: extract_table_name(class_attrs), + default_order: extract_source_order(class_attrs), } } } @@ -81,19 +84,26 @@ pub(crate) fn extract_clap_app(attrs: &[Attribute]) -> NormalClapAppInfo { } pub(crate) fn extract_env_prefix(attrs: &[Attribute]) -> Option { - attrs + match attrs .iter() .find(|a| compare_attribute_name(a, ENV_PREFIX_KEY)) - .map(|atr| match atr.parse_meta() { + { + None => Some(String::new()), + Some(attr) => match attr.parse_meta() { Err(err) => panic!("Can't parse attribute as meta: {err}"), Ok(meta) => match meta { + Meta::Path(_) => None, Meta::NameValue(MetaNameValue { lit: Lit::Str(input_name), .. - }) => input_name.value(), - _ => panic!("{ENV_PREFIX_KEY} must match #[{ENV_PREFIX_KEY} = \"...\"]"), + }) => Some(input_name.value()), + _ => panic!( + "{ENV_PREFIX_KEY} must match #[{ENV_PREFIX_KEY} = \"...\"] or \ + #[{ENV_PREFIX_KEY}]" + ), }, - }) + }, + } } pub(crate) fn extract_debug_cmd_input(attrs: &[Attribute]) -> Option { @@ -124,3 +134,48 @@ pub(crate) fn extract_table_name(attrs: &[Attribute]) -> Option { }, }) } + +pub(crate) fn extract_source_order(attrs: &[Attribute]) -> Option { + attrs + .iter() + .find(|a| compare_attribute_name(a, SOURCE_ORDER_KEY)) + .map(|atr| match atr.parse_meta() { + Err(err) => panic!("Can't parse attribute as meta: {err}"), + Ok(meta) => match meta { + Meta::List(list) => { + let mut res = ExtractedAttributes::default(); + for meta in list.nested { + match meta { + NestedMeta::Meta(Meta::Path(p)) => match path_to_string(&p).as_str() { + CLAP_KEY => { + res.variables.push(FieldAttribute::Clap(Default::default())) + } + ENV_KEY => { + res.variables.push(FieldAttribute::Env(Default::default())) + } + CONFIG_KEY => res + .variables + .push(FieldAttribute::Config(Default::default())), + DEFAULT => { + res.default = + Some(crate::utils::field::utils::Default::default()) + } + other => panic!( + "Error in \"{other}\" attribute: only {CLAP_KEY}, {ENV_KEY}, \ + {CONFIG_KEY} and {DEFAULT} are allowed as default_order \ + nested attribute" + ), + }, + other => panic!( + "default_order nested attributes can be on of: {CLAP_KEY}, \ + {ENV_KEY}, {CONFIG_KEY} and {DEFAULT}, error in meta: {}", + other.to_token_stream() + ), + } + } + res + } + _ => panic!("{SOURCE_ORDER_KEY} must match #[{SOURCE_ORDER_KEY}(...)]"), + }, + }) +} diff --git a/cookbook.md b/cookbook.md index 0f3eec6..3b3db23 100644 --- a/cookbook.md +++ b/cookbook.md @@ -5,11 +5,11 @@ - [Note](#note) - [Options](#options) - [Structure attributes](#structure-attributes) - - [`global name`](#global-name) - [`env_prefix`](#env_prefix) - [`file`](#file) - [`clap`](#clap) - [`table`](#table) + - [`default_order`](#default_order) - [Field attributes](#field-attributes) - [Source](#source) - [`default`](#default) @@ -103,11 +103,8 @@ The key point here is the fact that the options take precedence over the corresp More information can be found in the `ConfigOption` documentation. ## Structure attributes -### `global name` -If assigned, a global variable with the specified name will be created instead of deriving ConfigInit trait. - ### `env_prefix` -Prefix of the environment variables. The default prefix is the binary file name. +Prefix of the environment variables. If not stated, the prefix will not be added. Thus, the `iter` field in the example below will be searched in the environment by the `demo_iter` key. ```rust #[config( @@ -120,7 +117,8 @@ struct AppConfig { ``` **Notes** - The delimiter ('_') is placed automatically -- If a prefix isn't required, set `env_prefix = ""` +- `env_prefix = ""` will not add any prefix +- One can use `env_prefix` (without a value) to set the binary file name as a prefix - `env`, `env_prefix` and similar attributes are case-insensitive. If both the `demo_iter` and `DEMO_ITER` environment variables are present, which of these two will be parsed *is not defined* @@ -168,15 +166,37 @@ struct Config { ``` Field `frames` will be searched in the "input.data" table of the configuration file "config.toml". +### `default_order` +The default order of any field that wasn't annotated with any of `source`,`flatten` or `subcommand`.\ +`clap`, `env`, `config` and `default` are all possible parameters. +Each attribute will be applied to each unannotated field in a "short" form +(i.e., form without value; for example, `#[source(default)]` means that +`Default::default()` will be used as a default value. See the [source](#source) section for more information) +**Example** +```rust +#[config(default_order(env, clap, default))] +struct Config { + rotation: f32, +} +``` +It will be checked that the `ROTATION` environment variable is set; if not, the `--rotation` command line argument will be checked, +and, lastly, the `Default::default()` will be assigned. +**Note:** If this attribute isn't set, the default order is: +1. command line +2. environment variables +3. configuration files + ## Field attributes Only fields can be annotated with the following attributes and only one of them can be assigned to a field. +**Note:** if a field is not annotated with any of the following attributes, +it will be parsed using the default source order (see the section above). ### Source If a field is annotated with the `source` attribute, at least one of the following nested attributes must be present. #### `default` -Numeric literal or valid Rust code. -If the field's type implement `std::default::Default`, the attribute can be set without value. +Numeric literal or valid Rust code.\ +If the attribute is set without a value (`#[source(default)]`), the default value is `std::default::Default`. **Example** ```rust @@ -191,12 +211,14 @@ struct AppConfig { ``` #### `env` -Name of the environment variable to set the value from. If present, `env_prefix` (see above) -is ignored. The case is ignored. +The name of the environment variable from which the value is to be set. +`env_prefix` (see above) is ignored if present with a value (`#[source(env = "...")]`). The case is ignored. \ +If the attribute is set without value, the name of the environment variable to be set is `env_prefix + field_name`. #### `config` Name of the configuration file field to set the value from. It can contain dots: in this case the name will be parsed as a path to the field.\ +If the attribute is set without a value (`#[source(config)]`), the field name is the name of the configuration file field to be set. \ **Example** ```rust #[config(file(format = "toml", default = "./config.toml"), table = "input.data")] @@ -211,8 +233,10 @@ configuration file by the `frame_rate` key. #### `clap` Clap-crate attributes. Available nested attributes: `help`, `long_help`, `short`, `long`, `flatten`, `subcommand`. -**Note:** the default `long` and `short` values (`#[clap(long)]` and `#[clap(short)]`) is the field name and it's first letter. +**Note:** the default `long` and `short` values (`#[clap(long)]` and `#[clap(short)]`) is the field name and it's first letter. \ +`#[source(clap)]` is equivalent to `#[source(clap(long))]` \ +In addition, the following attribute can be used. #### `deserialize_with` Custom deserialization of the field. The deserialization function must have the signature ```rust @@ -268,7 +292,7 @@ struct NestedConfig { #### Flatten attributes Flatten struct may have the following helper attributes: `table`, `flatten`, `source` (they work the same way as the described above ones). ### Subcommand -If a field is annotated with the `flatten` attribute, it will be taken as a `clap` subcommand +If a field is annotated with the `subcommand` attribute, it will be taken as a `clap` subcommand (see [clap documentation](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#subcommands) for more info). The field's type must implement `clap::Subcommand` and `serde::Deserialize`. diff --git a/src/__cookbook.rs b/src/__cookbook.rs index 7b5a240..cd1fb3e 100644 --- a/src/__cookbook.rs +++ b/src/__cookbook.rs @@ -6,11 +6,11 @@ //! 2. [Intro](#intro) //! 3. [Options](#options) //! 4. [Structure level attributes](#structure-attributes) -//! 1. [global_name](#global-name) -//! 2. [env_prefix](#env_prefix) -//! 3. [file](#file) -//! 4. [clap](#clap) -//! 5. [table](#table) +//! 1. [env_prefix](#env_prefix) +//! 2. [file](#file) +//! 3. [clap](#clap) +//! 4. [table](#table) +//! 5. [default source order](#default_order) //! 5. [Field level attributes](#field-attributes) //! 1. [source](#source) //! - [default](#default) @@ -105,11 +105,8 @@ //! More information can be found in the [ConfigOption](../enum.ConfigOption.html) documentation. //! //! ## Structure attributes -//! ### `global name` -//! If assigned, a global variable with the specified name will be created instead of deriving ConfigInit trait. -//! //! ### `env_prefix` -//! Prefix of the environment variables. The default prefix is the binary file name. +//! Prefix of the environment variables. If not stated, the prefix will not be added. //! Thus, the `iter` field in the example below will be searched in the environment by the `demo_iter` key. //! ``` //! # use config_manager::config; @@ -124,7 +121,8 @@ //! ``` //! **Notes** //! - The delimiter ('_') is placed automatically -//! - If a prefix isn't required, set `env_prefix = ""` +//! - `env_prefix = ""` will not add any prefix +//! - One can use `env_prefix` (without a value) to set the binary file name as a prefix //! - `env`, `env_prefix` and similar attributes are case-insensitive. If both the `demo_iter` and //! `DEMO_ITER` environment variables are present, which of these two will be parsed *is not defined* //! @@ -176,15 +174,40 @@ //! ``` //! Field `frames` will be searched in the "input.data" table of the configuration file "config.toml". //! +//! ### `default_order` +//! The default order of any field that wasn't annotated with any of `source`,`flatten` or `subcommand`.\ +//! `clap`, `env`, `config` and `default` are all possible parameters. +//! Each attribute will be applied to each unannotated field in a "short" form +//! (i.e., form without value; for example, `#[source(default)]` means that +//! `Default::default()` will be used as a default value. See the [source](#source) section for more information) +//! **Example** +//! ``` +//! # use config_manager::config; +//! # +//! #[config(default_order(env, clap, default))] +//! struct Config { +//! rotation: f32, +//! } +//! ``` +//! It will be checked that the `ROTATION` environment variable is set; if not, the `--rotation` command line argument will be checked, +//! and, lastly, the `Default::default()` will be assigned. +//! **Note:** If this attribute isn't set, the default order is: +//! 1. command line +//! 2. environment variables +//! 3. configuration files +//! //! ## Field attributes //! Only fields can be annotated with the following attributes and only one of them can be assigned to a field. //! +//! **Note:** if a field is not annotated with any of the following attributes, +//! it will be parsed using the default source order (see the section above). +//! //! ### Source //! If a field is annotated with the `source` attribute, at least one of the following nested attributes must be present. //! //! #### `default` -//! Numeric literal or valid Rust code. -//! If the field's type implement `std::default::Default`, the attribute can be set without value. +//! Numeric literal or valid Rust code. \ +//! If the attribute is set without a value (`#[source(default)]`), the default value is `std::default::Default`. //! //! **Example** //! ``` @@ -201,12 +224,14 @@ //! ``` //! //! #### `env` -//! Name of the environment variable to set the value from. If present, `env_prefix` (see above) -//! is ignored. The case is ignored. +//! The name of the environment variable from which the value is to be set. +//! `env_prefix` (see above) is ignored if present with a value (`#[source(env = "...")]`). The case is ignored. \ +//! If the attribute is set without value, the name of the environment variable to be set is `env_prefix + field_name`. //! //! #### `config` //! Name of the configuration file field to set the value from. It can contain dots: in this case //! the name will be parsed as the path of the field.\ +//! If the attribute is set without a value (`#[source(config)]`), the field name is the name of the configuration file field to be set. \ //! **Example** //! ``` //! # use config_manager::config; @@ -223,8 +248,10 @@ //! #### `clap` //! Clap-crate attributes. Available nested attributes: `help`, `long_help`, `short`, `long`, //! `flatten`, `subcommand`. -//! **Note:** the default `long` and `short` values (`#[clap(long)]` and `#[clap(short)]`) is the field name and it's first letter respectively. +//! **Note:** the default `long` and `short` values (`#[clap(long)]` and `#[clap(short)]`) is the field name and it's first letter respectively. \ +//! `#[source(clap)]` is equivalent to `#[source(clap(long))]` \ //! +//! In addition, the following attribute can be used. //! #### `deserialize_with` //! Custom deserialization of the field. The deserialization function must have the signature //! ```ignore @@ -283,7 +310,7 @@ //! #### Flatten attributes //! Flatten struct may have the following helper attributes: `table`, `flatten`, `source` (they work the same way as the described above ones). //! ### Subcommand -//! If a field is annotated with the `flatten` attribute, it will be taken as a `clap` subcommand +//! If a field is annotated with the `subcommand` attribute, it will be taken as a `clap` subcommand //! (see [clap documentation](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#subcommands) for more info). //! The field's type must implement `clap::Subcommand` and `serde::Deserialize`. //! diff --git a/tests/data/config.toml b/tests/data/config.toml index 6b3a821..f9b8c1f 100644 --- a/tests/data/config.toml +++ b/tests/data/config.toml @@ -5,6 +5,7 @@ some = 999999999999999 debug_mode = true toml = 2 config = 3 +int_env = 100 [input] int = 5 diff --git a/tests/parse_method/env.rs b/tests/parse_method/env.rs index c021ebd..e539d09 100644 --- a/tests/parse_method/env.rs +++ b/tests/parse_method/env.rs @@ -72,7 +72,7 @@ fn env() { #[test] fn env_prefix() { - test_env(vec![empty_prefix, no_prefix, some_prefix]) + test_env(vec![empty_prefix, no_prefix, some_prefix, binary_prefix]) } fn empty_prefix() { @@ -111,7 +111,6 @@ fn some_prefix() { }); } -/// bin file is like config-manager/target/debug/deps/parse_method-b5e125d4f8a36dad fn no_prefix() { #[derive(Debug, PartialEq)] #[config(__debug_cmd_input__())] @@ -124,5 +123,23 @@ fn no_prefix() { set_env("fir", 1); set_env("second", 2); - assert!(matches!(NoPrefix::parse(), Err(Error::MissingArgument(_)))); + assert_ok_and_compare(&NoPrefix { + first: 1, + second: 2, + }); +} + +/// bin file is like config-manager/target/debug/deps/parse_method-b5e125d4f8a36dad +fn binary_prefix() { + #[config(env_prefix, __debug_cmd_input__())] + struct BinPrefix { + #[allow(dead_code)] + #[source(env)] + first: String, + } + + set_env("first", 1); + + let parsed = BinPrefix::parse(); + assert!(matches!(parsed, Err(Error::MissingArgument(_)))); } diff --git a/tests/parse_method/layers.rs b/tests/parse_method/layers.rs index 51b174c..de7ec1c 100644 --- a/tests/parse_method/layers.rs +++ b/tests/parse_method/layers.rs @@ -90,3 +90,52 @@ fn short_sources() { config: 3, }) } + +#[test] +fn default_priority() { + #[derive(Debug, PartialEq)] + #[config( + file(format = "toml", default = "./tests/data/config.toml"), + env_prefix = "", + __debug_cmd_input__("--int=0") + )] + struct Cfg { + int: i32, + int_env: i32, + toml: i32, + } + + set_env("int_env", 1); + set_env("int", 1000); + + assert_ok_and_compare(&Cfg { + int: 0, + int_env: 1, + toml: 2, + }) +} + +#[test] +fn custom_priority() { + #[derive(Debug, PartialEq)] + #[config( + file(format = "toml", default = "./tests/data/config.toml"), + default_order(env, config, clap, default), + __debug_cmd_input__("--int_env=0", "--toml=0", "--clap=1") + )] + struct Cfg { + int_env: i32, + toml: i32, + clap: i32, + default: Vec, + } + + set_env("int_env", 3); + + assert_ok_and_compare(&Cfg { + int_env: 3, + toml: 2, + clap: 1, + default: Default::default(), + }) +}