From 8b33912f54c519bd78ddcd3bb267c8d658ed254a Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Mon, 27 Feb 2023 15:36:23 +0300 Subject: [PATCH 1/8] NI: add default field annotation --- config-manager-proc/src/lib.rs | 18 +++--------------- config-manager-proc/src/utils.rs | 8 -------- config-manager-proc/src/utils/field.rs | 9 +-------- config-manager-proc/src/utils/field/utils.rs | 8 ++++++++ 4 files changed, 12 insertions(+), 31 deletions(-) diff --git a/config-manager-proc/src/lib.rs b/config-manager-proc/src/lib.rs index 599aa44..0ac07df 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. \ @@ -85,14 +85,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) }; ((name, initialization), clap_field) }) @@ -140,14 +134,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) }; ((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/field.rs b/config-manager-proc/src/utils/field.rs index 629b29e..ea82946 100644 --- a/config-manager-proc/src/utils/field.rs +++ b/config-manager-proc/src/utils/field.rs @@ -29,16 +29,9 @@ 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 { 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: \ \"{}\")", diff --git a/config-manager-proc/src/utils/field/utils.rs b/config-manager-proc/src/utils/field/utils.rs index 1c183c5..bbbf45b 100644 --- a/config-manager-proc/src/utils/field/utils.rs +++ b/config-manager-proc/src/utils/field/utils.rs @@ -193,6 +193,7 @@ impl Display for FieldAttribute { } } +#[derive(Default)] pub(super) struct Env { pub(super) inner: Option, } @@ -230,6 +231,7 @@ impl Env { } } +#[derive(Default)] pub(super) struct Config { pub(super) key: Option, pub(super) table: Option, @@ -343,6 +345,12 @@ pub(super) fn extract_attributes(field: Field, table_name: Option) -> Ex _ => panic!("source attribute must match #[source(...)]"), }, } + } else { + res.variables = vec![ + FieldAttribute::Clap(ClapFieldParseResult::default()), + FieldAttribute::Env(Env::default()), + FieldAttribute::Config(Config::default()), + ]; } res From ff31d1e9561c64c7d83b002ecbaf6634364f69fc Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Mon, 27 Feb 2023 15:59:37 +0300 Subject: [PATCH 2/8] NI: add test --- CHANGELOG.md | 5 +++++ tests/data/config.toml | 1 + tests/parse_method/layers.rs | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+) 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/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/layers.rs b/tests/parse_method/layers.rs index 51b174c..cc63e86 100644 --- a/tests/parse_method/layers.rs +++ b/tests/parse_method/layers.rs @@ -90,3 +90,27 @@ 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, + }) +} From 6c636edbe36bc261730830a0d6e11c93bbec4b41 Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Tue, 28 Feb 2023 12:39:37 +0300 Subject: [PATCH 3/8] #2: no env prefix by default --- config-manager-proc/src/utils/top_level.rs | 17 +++++++++++----- tests/parse_method/env.rs | 23 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/config-manager-proc/src/utils/top_level.rs b/config-manager-proc/src/utils/top_level.rs index 48e686a..5f73c61 100644 --- a/config-manager-proc/src/utils/top_level.rs +++ b/config-manager-proc/src/utils/top_level.rs @@ -81,19 +81,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 { 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(_)))); } From 03acf434f66f46393aee9db448fc02788302a7dc Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Tue, 28 Feb 2023 13:53:25 +0300 Subject: [PATCH 4/8] #2: fix readme --- cookbook.md | 19 +++++++++++-------- src/__cookbook.rs | 28 ++++++++++++++++------------ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/cookbook.md b/cookbook.md index 0f3eec6..6238394 100644 --- a/cookbook.md +++ b/cookbook.md @@ -5,7 +5,6 @@ - [Note](#note) - [Options](#options) - [Structure attributes](#structure-attributes) - - [`global name`](#global-name) - [`env_prefix`](#env_prefix) - [`file`](#file) - [`clap`](#clap) @@ -103,11 +102,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 +116,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* @@ -171,6 +168,11 @@ Field `frames` will be searched in the "input.data" table of the configuration f ## 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:\ +1. clap +2. env +3. config ### Source If a field is annotated with the `source` attribute, at least one of the following nested attributes must be present. @@ -211,8 +213,9 @@ 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. \ +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 +271,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..b0ce7fa 100644 --- a/src/__cookbook.rs +++ b/src/__cookbook.rs @@ -6,11 +6,10 @@ //! 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. [Field level attributes](#field-attributes) //! 1. [source](#source) //! - [default](#default) @@ -105,11 +104,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 +120,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* //! @@ -179,6 +176,12 @@ //! ## 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:\ +//! 1. clap +//! 2. env +//! 3. config +//! //! ### Source //! If a field is annotated with the `source` attribute, at least one of the following nested attributes must be present. //! @@ -223,8 +226,9 @@ //! #### `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. \ //! +//! 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 +287,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`. //! From 21bd557106dcaf6e52ec4883c57e12d7aa5ef6e3 Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Tue, 28 Feb 2023 16:08:35 +0300 Subject: [PATCH 5/8] #2: add default_order annotation --- config-manager-proc/src/lib.rs | 6 +- config-manager-proc/src/utils/attributes.rs | 1 + config-manager-proc/src/utils/field.rs | 19 +- config-manager-proc/src/utils/field/utils.rs | 208 ++++++++++--------- config-manager-proc/src/utils/parser/clap.rs | 4 +- config-manager-proc/src/utils/top_level.rs | 48 +++++ 6 files changed, 181 insertions(+), 105 deletions(-) diff --git a/config-manager-proc/src/lib.rs b/config-manager-proc/src/lib.rs index 0ac07df..29e655c 100644 --- a/config-manager-proc/src/lib.rs +++ b/config-manager-proc/src/lib.rs @@ -62,6 +62,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 { @@ -86,7 +87,7 @@ pub fn generate_config(input: TokenStream0) -> TokenStream0 { } else if field_is_subcommand(&field) { process_subcommand_field(field, &debug_cmd_input) } else { - process_field(field, &table_name) + process_field(field, &table_name, &default_order) }; ((name, initialization), clap_field) }) @@ -111,6 +112,7 @@ 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 { @@ -135,7 +137,7 @@ pub fn generate_flatten(input: TokenStream0) -> TokenStream0 { } else if field_is_subcommand(&field) { panic!("subcommands are forbidden in the nested structures") } else { - process_field(field, &table_name) + process_field(field, &table_name, &default_order) }; ((name, initialization), clap_field) }) 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/field.rs b/config-manager-proc/src/utils/field.rs index ea82946..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,7 +29,11 @@ pub(crate) struct ProcessFieldResult { pub(crate) initialization: TokenStream, } -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 { panic!( @@ -39,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 bbbf45b..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,8 +194,8 @@ impl Display for FieldAttribute { } } -#[derive(Default)] -pub(super) struct Env { +#[derive(Default, Clone)] +pub(crate) struct Env { pub(super) inner: Option, } @@ -231,8 +232,8 @@ impl Env { } } -#[derive(Default)] -pub(super) struct Config { +#[derive(Default, Clone)] +pub(crate) struct Config { pub(super) key: Option, pub(super) table: Option, } @@ -251,107 +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(...)]"), - }, - } - } else { - res.variables = vec![ - FieldAttribute::Clap(ClapFieldParseResult::default()), - FieldAttribute::Env(Env::default()), - FieldAttribute::Config(Config::default()), - ]; - } + _ => 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/top_level.rs b/config-manager-proc/src/utils/top_level.rs index 5f73c61..f2e57ed 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), } } } @@ -131,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::Lit(Lit::Str(lit)) => match lit.value().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 must be literals, error in meta: \ + {}", + other.to_token_stream() + ), + } + } + res + } + _ => panic!("{SOURCE_ORDER_KEY} must match #[{SOURCE_ORDER_KEY}(...)]"), + }, + }) +} From 67761bc6157dd10f760858d61ee88f6c281c167f Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Tue, 28 Feb 2023 16:20:26 +0300 Subject: [PATCH 6/8] #2: fixes and test on default order --- config-manager-proc/src/lib.rs | 3 ++- config-manager-proc/src/utils/top_level.rs | 6 +++--- tests/parse_method/layers.rs | 25 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/config-manager-proc/src/lib.rs b/config-manager-proc/src/lib.rs index 29e655c..4085e7c 100644 --- a/config-manager-proc/src/lib.rs +++ b/config-manager-proc/src/lib.rs @@ -48,6 +48,7 @@ pub fn config(attrs: TokenStream0, input: TokenStream0) -> TokenStream0 { global_name, file, table, + default_order, __debug_cmd_input__ ) )] @@ -107,7 +108,7 @@ 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); diff --git a/config-manager-proc/src/utils/top_level.rs b/config-manager-proc/src/utils/top_level.rs index f2e57ed..4f7c8dc 100644 --- a/config-manager-proc/src/utils/top_level.rs +++ b/config-manager-proc/src/utils/top_level.rs @@ -146,7 +146,7 @@ pub(crate) fn extract_source_order(attrs: &[Attribute]) -> Option match lit.value().as_str() { + NestedMeta::Meta(Meta::Path(p)) => match path_to_string(&p).as_str() { CLAP_KEY => { res.variables.push(FieldAttribute::Clap(Default::default())) } @@ -167,8 +167,8 @@ pub(crate) fn extract_source_order(attrs: &[Attribute]) -> Option panic!( - "default_order nested attributes must be literals, error in meta: \ - {}", + "default_order nested attributes can be on of: {CLAP_KEY}, \ + {ENV_KEY}, {CONFIG_KEY} and {DEFAULT}, error in meta: {}", other.to_token_stream() ), } diff --git a/tests/parse_method/layers.rs b/tests/parse_method/layers.rs index cc63e86..de7ec1c 100644 --- a/tests/parse_method/layers.rs +++ b/tests/parse_method/layers.rs @@ -114,3 +114,28 @@ fn default_priority() { 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(), + }) +} From 32b7788957e86938af0a15430e7c9c8fd0dd88ab Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Wed, 1 Mar 2023 11:48:50 +0300 Subject: [PATCH 7/8] #2: Update Cookbook --- cookbook.md | 37 +++++++++++++++++++++++++++++-------- src/__cookbook.rs | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/cookbook.md b/cookbook.md index 6238394..3b3db23 100644 --- a/cookbook.md +++ b/cookbook.md @@ -9,6 +9,7 @@ - [`file`](#file) - [`clap`](#clap) - [`table`](#table) + - [`default_order`](#default_order) - [Field attributes](#field-attributes) - [Source](#source) - [`default`](#default) @@ -165,20 +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:\ -1. clap -2. env -3. config +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 @@ -193,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")] @@ -214,6 +234,7 @@ configuration file by the `frame_rate` key. 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. \ +`#[source(clap)]` is equivalent to `#[source(clap(long))]` \ In addition, the following attribute can be used. #### `deserialize_with` diff --git a/src/__cookbook.rs b/src/__cookbook.rs index b0ce7fa..cd1fb3e 100644 --- a/src/__cookbook.rs +++ b/src/__cookbook.rs @@ -10,6 +10,7 @@ //! 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) @@ -173,21 +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:\ -//! 1. clap -//! 2. env -//! 3. config +//! 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** //! ``` @@ -204,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; @@ -227,6 +249,7 @@ //! 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. \ +//! `#[source(clap)]` is equivalent to `#[source(clap(long))]` \ //! //! In addition, the following attribute can be used. //! #### `deserialize_with` From eddcf65392aa2cf1e39ef8afb036388a657ed4cd Mon Sep 17 00:00:00 2001 From: Mikhail Mikhailov Date: Wed, 1 Mar 2023 15:29:31 +0300 Subject: [PATCH 8/8] #2: refactor AsRef --- config-manager-proc/src/utils/config.rs | 10 +++++----- config-manager-proc/src/utils/parser/utils.rs | 5 +---- 2 files changed, 6 insertions(+), 9 deletions(-) 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/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) }