diff --git a/examples/demo/Cargo.toml b/examples/demo/Cargo.toml index 7702c6a..4b98c0a 100644 --- a/examples/demo/Cargo.toml +++ b/examples/demo/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" publish = false [dependencies] -more-config = { path = "../../src", features = ["mem", "env", "cmd", "json", "binder"] } -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +more-config = { path = "../../src", features = ["mem", "env", "cmd", "json", "binder", "struct"] } +serde = { version = "1.0", features = ["derive"] } +serde-intermediate = "1.6" diff --git a/examples/demo/src/main.rs b/examples/demo/src/main.rs index c6fbf33..501b1b6 100644 --- a/examples/demo/src/main.rs +++ b/examples/demo/src/main.rs @@ -1,8 +1,9 @@ use config::{*, ext::*}; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; #[allow(dead_code)] -#[derive(Default, Deserialize)] +#[derive(Default, Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "PascalCase"))] #[serde(rename_all(deserialize = "PascalCase"))] struct Client { region: String, @@ -10,11 +11,21 @@ struct Client { } #[allow(dead_code)] -#[derive(Default, Deserialize)] +#[derive(Default, Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "PascalCase"))] +#[serde(rename_all(deserialize = "PascalCase"))] +struct SubOptions { + value: i32, +} + +#[allow(dead_code)] +#[derive(Default, Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "PascalCase"))] #[serde(rename_all(deserialize = "PascalCase"))] struct AppOptions { text: String, demo: bool, + sub: SubOptions, clients: Vec, } @@ -24,7 +35,16 @@ fn main() { .parent() .unwrap() .join("../../examples/demo/demo.json"); + + let default = AppOptions { + text: String::from("Default text"), + demo: false, + sub: SubOptions { value: 34}, + clients: Vec::new(), + }; + let config = DefaultConfigurationBuilder::new() + .add_struct(default.clone()) .add_in_memory(&[("Demo", "false")]) .add_json_file(file) .add_env_vars() @@ -36,8 +56,10 @@ fn main() { if app.demo { println!("{}", &app.text); println!("{}", &app.clients[0].region); + println!("Suboption value by query: {}", config.get("Sub:Value").unwrap().as_str()); + println!("Suboption value by binding: {}", &app.sub.value); return; } println!("Not a demo!"); -} \ No newline at end of file +} diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index ac3de0e..9aad71a 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -12,10 +12,11 @@ - [Working With Data](guide/data.md) - [File Sources](guide/files.md) - [In-Memory Provider](guide/memory.md) +- [Data Structure Provider](guide/struct.md) - [Environment Variable Provider](guide/env.md) - [Command-Line Provider](guide/cmd.md) - [JSON Provider](guide/json.md) - [XML Provider](guide/xml.md) - [INI Provider](guide/ini.md) - [Chained Provider](guide/chained.md) -- [Data Binding](guide/binding.md) \ No newline at end of file +- [Data Binding](guide/binding.md) diff --git a/guide/src/guide/struct.md b/guide/src/guide/struct.md new file mode 100644 index 0000000..500dd1b --- /dev/null +++ b/guide/src/guide/struct.md @@ -0,0 +1,135 @@ +{{#include links.md}} + +# Data-Structure Configuration Provider + +>These features are only available if the **struct** feature is activated + +The [`StructConfigurationProvider`] uses an in-memory data structure as configuration key-value pairs. This is most useful as an alternative default configuration to in-memory configuration or when providing test values. + +The following code adds a data structure collection to the configuration system and displays the settings: + +```rust +use config::{*, ext::*}; +use serde::Serialize; + +#[derive(Default, Serialize, Clone)] +#[serde(rename_all(serialize = "PascalCase"))] +struct PerfSettings { + cores: u8, +} + +#[derive(Default, Serialize, Clone)] +#[serde(rename_all(serialize = "PascalCase"))] +struct AppOptions { + title: String, + perf: PerfSettings, +} + +fn main() { + let default = AppOptions { + title: String::from("Banana processor"), + perf: SubOptions{ cores: 7 }, + }; + + let config = DefaultConfigurationBuilder::new() + .add_struct(default.clone()) + .build() + .unwrap(); + + let title = config.get("title").unwrap().as_str(); + let cores = config.get("Perf:Cores").unwrap().as_str(); + + println!("Title: {}\n\ + Cores: {}\n\ + title, + cores); +} +``` + +Instead of a data structure, one can also load a tuple, a vector, or a map. +Values from Tuples and Vectors can be retrieved with their index as key: + +```rust +use config::{*, ext::*}; + +fn main() { + let value = std::vec::Vec::from([32, 56]); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + println!("[{}, {}]", config.get("0"), config.get("1")); +} +``` + +Or, same example with a tuple: + +```rust +use config::{*, ext::*}; + +fn main() { + let value = (32, 56); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + println!("[{}, {}]", config.get("0"), config.get("1")); +} +``` + +When loading maps, it is best to restrict to maps using strings as keys. When using strings as keys, it is possible to bind the data to some data structure: + +```rust +use config::{*, ext::*}; + +#[derive(Deserialize)] +#[serde(rename_all(deserialize = "PascalCase"))] +struct FooBar { + foo: String, + karoucho: i32, +} + +fn main() { + let mut value: HashMap<&str, &str> = HashMap::new(); + value.insert("foo", "bar"); + value.insert("karoucho", "34"); + + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let foo = config.get("foo"); + println!("foo by query: {}", foo); + let karoucho = config.get("karoucho"); + println!("karoucho by query: {}", karoucho); + + let options: FooBar = config.reify(); + println!("foo by binding: {}", options.foo); + println!("karoucho by binding: {}", options.karoucho); +} +``` + +When using non-strings as keys, `serde` appends type information to the key name. The suffix must be then given when querying the value: + + +```rust +use config::{*, ext::*}; + +fn main() { + let mut value: HashMap = HashMap::new(); + value.insert(-32, 56); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let loaded = config.get("-32_i32"); + assert_eq!(loaded.unwrap().as_str(), "56"); +} + +``` + +Also, note that binding is then not possible, since a data structure member name cannot start with a digit or a sign. +`bool` keys are not added any type suffix to the generated key, and can be used to bind into a data structure. +However, this can be applied in a limited number of scenarios and using non-string as keys is generally not supported. + + diff --git a/src/Cargo.toml b/src/Cargo.toml index 9336990..62ce3da 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -13,7 +13,7 @@ include = ["*.rs", "!build.rs", "README.md"] # RUSTDOCFLAGS="--cfg docsrs"; cargo +nightly doc [package.metadata.docs.rs] -features = ["std", "chained", "mem", "env", "cmd", "ini", "json", "xml", "binder"] +features = ["std", "chained", "mem", "env", "cmd", "ini", "json", "xml", "binder", "struct"] rustdoc-args = ["--cfg", "docsrs"] [lib] @@ -34,6 +34,7 @@ ini = ["util", "configparser", "more-changetoken/fs"] binder = ["serde"] json = ["util", "serde_json", "more-changetoken/fs"] xml = ["util", "xml_rs", "more-changetoken/fs"] +struct = ["serde-intermediate"] [dependencies] more-changetoken = "2.0" @@ -42,10 +43,11 @@ serde = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true } xml_rs = { version = "0.8", package = "xml", optional = true } cfg-if = "1.0" +serde-intermediate = { version = "1.6", optional = true } [dev-dependencies] test-case = "2.2" [dev-dependencies.more-config] path = "." -features = ["cmd"] \ No newline at end of file +features = ["cmd"] diff --git a/src/lib.rs b/src/lib.rs index 7a6cce0..9bf2292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,9 @@ mod binder; #[cfg(feature = "binder")] mod de; +#[cfg(feature = "struct")] +mod r#struct; + mod file; pub use builder::*; pub use configuration::*; @@ -97,6 +100,10 @@ pub use cmd::{CommandLineConfigurationProvider, CommandLineConfigurationSource}; #[cfg_attr(docsrs, doc(cfg(feature = "xml")))] pub use xml::{XmlConfigurationProvider, XmlConfigurationSource}; +#[cfg(feature = "struct")] +#[cfg_attr(docsrs, doc(cfg(feature = "struct")))] +pub use r#struct::{StructConfigurationProvider, StructConfigurationSource}; + /// Contains configuration extension methods. pub mod ext { @@ -138,6 +145,10 @@ pub mod ext { #[cfg_attr(docsrs, doc(cfg(feature = "binder")))] pub use de::*; + #[cfg(feature = "struct")] + #[cfg_attr(docsrs, doc(cfg(feature = "struct")))] + pub use r#struct::ext::*; + pub use section::ext::*; pub use file::ext::*; } diff --git a/src/struct.rs b/src/struct.rs new file mode 100644 index 0000000..79b3a6a --- /dev/null +++ b/src/struct.rs @@ -0,0 +1,246 @@ +use crate::{ + ConfigurationBuilder, ConfigurationPath, ConfigurationProvider, ConfigurationSource, + LoadError, LoadResult, Value, accumulate_child_keys, to_pascal_case +}; + +use serde_intermediate::value::intermediate::Intermediate; +use std::collections::HashMap; +use std::sync::RwLock; +use serde::Serialize; +use std::sync::Arc; + +#[derive(Default)] +struct StructVisitor { + data: HashMap, + paths: Vec, +} + +impl StructVisitor { + fn visit(mut self, root: &Intermediate) -> HashMap { + self.visit_element(root); + self.data.shrink_to_fit(); + self.data + } + + fn visit_element(&mut self, element: &Intermediate) { + match element { + Intermediate::Unit | + Intermediate::UnitStruct | + Intermediate::UnitVariant(_) => { + panic!("Unit and Unit struct are not supported"); + }, + _ => {} + }; + + match element { + Intermediate::Seq(vector) | + Intermediate::Tuple(vector) | + Intermediate::TupleVariant(_, vector) | + Intermediate::TupleStruct(vector) => { + if vector.len() > 0 + { + for (index, element) in vector.iter().enumerate() { + self.enter_context(index.to_string()); + self.visit_element(element); + self.exit_context(); + } + } else { + if let Some(key) = self.paths.last() { + self.data + .insert(key.to_uppercase(), (to_pascal_case(key), String::new().into())); + } + } + return + }, + _ => {} + }; + + match element { + Intermediate::Map(map) => { + if map.len() > 0 + { + for (name, element) in map { + self.enter_context(to_pascal_case(name.to_string().replace("\"", ""))); + self.visit_element(element); + self.exit_context(); + } + } else { + if let Some(key) = self.paths.last() { + self.data + .insert(key.to_uppercase(), (to_pascal_case(key), String::new().into())); + } + } + return + }, + Intermediate::Struct(map) | + Intermediate::StructVariant(_, map) => { + if map.len() > 0 + { + for (name, element) in map { + self.enter_context(name.to_string()); + self.visit_element(element); + self.exit_context(); + } + } else { + if let Some(key) = self.paths.last() { + self.data + .insert(key.to_uppercase(), (to_pascal_case(key), String::new().into())); + } + } + return + }, + _ => {} + }; + + match element { + Intermediate::Bool(v) => { self.add_value(v) }, + Intermediate::String(v) => { self.add_value(v) }, + Intermediate::I8(v) => { self.add_value(v) }, + Intermediate::I16(v) => { self.add_value(v) }, + Intermediate::I32(v) => { self.add_value(v) }, + Intermediate::I64(v) => { self.add_value(v) }, + Intermediate::I128(v) => { self.add_value(v) }, + Intermediate::U8(v) => { self.add_value(v) }, + Intermediate::U16(v) => { self.add_value(v) }, + Intermediate::U32(v) => { self.add_value(v) }, + Intermediate::U64(v) => { self.add_value(v) }, + Intermediate::U128(v) => { self.add_value(v) }, + Intermediate::F32(v) => { self.add_value(v) }, + Intermediate::F64(v) => { self.add_value(v) }, + Intermediate::Char(v) => { self.add_value(v) }, + Intermediate::Option(v) => { + match v { + Some(v) => self.add_value(v), + None => {}, + }; + }, + _ => { std::unreachable!(); } + }; + } + + fn add_value(&mut self, value: T) { + let key = self.paths.last().unwrap().to_string(); + self.data + .insert(key.to_uppercase(), (key, value.to_string().into())); + } + + fn enter_context(&mut self, context: String) { + if self.paths.is_empty() { + self.paths.push(context); + return; + } + + let path = ConfigurationPath::combine(&[&self.paths[self.paths.len() - 1], &context]); + self.paths.push(path); + } + + fn exit_context(&mut self) { + self.paths.pop(); + } +} + +struct InnerProvider { + r#struct: Box, + data: RwLock>, +} + +impl InnerProvider { + fn new(r#struct: T) -> Self { + Self { + r#struct: Box::new(r#struct), + data: RwLock::new(HashMap::with_capacity(0)), + } + } + + fn load(&self) -> LoadResult { + let visitor = StructVisitor::default(); + let data = visitor.visit(match &serde_intermediate::to_intermediate(&self.r#struct) { + Ok(v) => v, + Err(_) => { Err(LoadError::Generic(String::from("Could not serialize data")))? }, + }); + *match self.data.write() { + Ok(ptr) => ptr, + Err(_) => { Err(LoadError::Generic(String::from("Could not get lock to write data")))? }, + } = data; + Ok(()) + } + + fn get(&self, key: &str) -> Option { + self.data + .read() + .unwrap() + .get(&key.to_uppercase()) + .map(|t| t.1.clone()) + } + + fn child_keys(&self, earlier_keys: &mut Vec, parent_path: Option<&str>) { + let data = self.data.read().unwrap(); + accumulate_child_keys(&data, earlier_keys, parent_path) + } +} + +pub struct StructConfigurationProvider { + inner: Arc>, +} + +impl StructConfigurationProvider { + pub fn new(obj: T) -> Self { + let obj = Self { + inner: Arc::new(InnerProvider::new(obj)) + }; + let _ = obj.inner.load(); + obj + } +} + +impl ConfigurationProvider for StructConfigurationProvider { + fn get(&self, key: &str) -> Option { + self.inner.get(key) + } + + fn child_keys(&self, earlier_keys: &mut Vec, parent_path: Option<&str>) { + self.inner.child_keys(earlier_keys, parent_path) + } +} + +pub struct StructConfigurationSource { + obj: T +} + +impl StructConfigurationSource { + pub fn new(obj: T) -> Self { + Self { + obj + } + } +} + +impl ConfigurationSource for StructConfigurationSource { + fn build(&self, _builder: &dyn ConfigurationBuilder) -> Box { + Box::new(StructConfigurationProvider::new(self.obj.clone())) + } +} + +pub mod ext { + + use super::*; + + pub trait StructConfigurationExtensions { + fn add_struct(&mut self, r#struct: T) -> &mut Self; + } + + impl StructConfigurationExtensions<> for dyn ConfigurationBuilder { + fn add_struct(&mut self, r#struct: T) -> &mut Self { + self.add(Box::new(StructConfigurationSource::new(r#struct))); + self + } + } + + impl StructConfigurationExtensions for T { + fn add_struct(&mut self, r#struct: F) -> &mut Self { + self.add(Box::new(StructConfigurationSource::new(r#struct))); + self + } + } +} + diff --git a/test/Cargo.toml b/test/Cargo.toml index 10176aa..df3116c 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -10,7 +10,8 @@ doctest = false [dependencies] more-changetoken = "2.0" -more-config = { path = "../src", features = ["std", "chained", "mem", "env", "ini", "json", "xml", "binder"] } +more-config = { path = "../src", features = ["std", "chained", "mem", "env", "ini", "json", "xml", "binder", "struct"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -test-case = "2.2" \ No newline at end of file +test-case = "2.2" +serde-intermediate = "1.6" diff --git a/test/lib.rs b/test/lib.rs index b575660..39e9132 100644 --- a/test/lib.rs +++ b/test/lib.rs @@ -8,3 +8,4 @@ mod ini; mod json; mod reload; mod xml; +mod r#struct; diff --git a/test/struct.rs b/test/struct.rs new file mode 100644 index 0000000..3880e17 --- /dev/null +++ b/test/struct.rs @@ -0,0 +1,247 @@ +use config::{ext::*, *}; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; + +#[test] +#[should_panic] +fn load_unit_struct() { + // Serializing empty struct is unsupported + #[derive(Deserialize, Serialize, Clone)] + #[serde(rename_all(serialize = "PascalCase"))] + #[serde(rename_all(deserialize = "PascalCase"))] + struct A; + let aa: A = A{}; + + let _ = DefaultConfigurationBuilder::new() + .add_struct(aa) + .build() + .unwrap(); +} + +#[test] +fn load_simple_struct() { + #[derive(Deserialize, Serialize, Clone)] + #[serde(rename_all(serialize = "PascalCase"))] + #[serde(rename_all(deserialize = "PascalCase"))] + struct A { + a: i32, + b: bool, + } + let aa = A { + a: 32, + b: false + }; + + let config = DefaultConfigurationBuilder::new() + .add_struct(aa.clone()) + .build() + .unwrap(); + assert_eq!(config.get("a").unwrap().as_str(), "32"); + assert_eq!(config.get("b").unwrap().as_str(), "false"); + let bb: A = config.reify(); + assert_eq!(aa.a, bb.a); + assert_eq!(aa.b, bb.b); +} + +#[test] +#[should_panic] +fn load_bool() { + // Serializing primitive types is not supported + for value in [true, false] { + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let loaded = config.get(""); + assert_eq!(loaded.unwrap().as_str(), value.to_string().as_str()); + } +} + +#[test] +fn load_map_str_str() { + let mut value: HashMap<&str, &str> = HashMap::new(); + value.insert("foo", "bar"); + value.insert("karoucho", "34"); + + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let loaded = config.get("foo"); + assert_eq!(loaded.unwrap().as_str(), "bar"); + + #[derive(Deserialize)] + #[serde(rename_all(deserialize = "PascalCase"))] + struct FooBar { + foo: String, + karoucho: i32, + } + let options: FooBar = config.reify(); + assert_eq!(options.foo, "bar".to_string()); + assert_eq!(options.karoucho, 34); +} + +#[test] +fn load_map_bool_i32() { + let mut value: HashMap = HashMap::new(); + value.insert(false, 56); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let loaded = config.get("false"); + assert_eq!(loaded.unwrap().as_str(), "56"); + + #[derive(Deserialize, Serialize, Clone)] + #[serde(rename_all(serialize = "PascalCase"))] + #[serde(rename_all(deserialize = "PascalCase"))] + struct BoolI32 { + r#false: i32 + } + + let my: BoolI32 = config.reify(); + assert_eq!(my.r#false, 56); +} + +#[test] +fn load_map_i32_i32() { + let mut value: HashMap = HashMap::new(); + value.insert(-32, 56); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let loaded = config.get("-32_i32"); + assert_eq!(loaded.unwrap().as_str(), "56"); + // Cannot reify into struct with "-32" as member name +} + +#[test] +fn load_tuple_bool_i32() { + let value = (false, 56); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "false"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "56"); +} + +#[test] +fn load_vec_i32() { + let value = std::vec::Vec::from([32, 56]); + let config = DefaultConfigurationBuilder::new() + .add_struct(value) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "32"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "56"); +} + +#[test] +fn load_vec_replaces_and_appends() { + let vec1 = std::vec::Vec::from([32, 56]); + let vec2 = std::vec::Vec::from([96, 3, 54]); + let config = DefaultConfigurationBuilder::new() + .add_struct(vec1) + .add_struct(vec2) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "96"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "3"); + let value1 = config.get("2"); + assert_eq!(value1.unwrap().as_str(), "54"); +} + +#[test] +fn load_explicit_vec_replace() { + let vec = std::vec::Vec::from([32, 56]); + let mut replace: HashMap<&str, i32> = HashMap::new(); + replace.insert("1", 96); + let config = DefaultConfigurationBuilder::new() + .add_struct(vec) + .add_struct(replace) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "32"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "96"); +} + +#[test] +fn load_vec_merge() { + let vec = std::vec::Vec::from([32, 56]); + let mut merge: HashMap<&str, i32> = HashMap::new(); + merge.insert("2", 96); + let config = DefaultConfigurationBuilder::new() + .add_struct(vec) + .add_struct(merge) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "32"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "56"); + let value1 = config.get("2"); + assert_eq!(value1.unwrap().as_str(), "96"); +} + +#[test] +fn load_vec_merge_to_far() { + let vec = std::vec::Vec::from([32, 56]); + let mut merge: HashMap<&str, i32> = HashMap::new(); + merge.insert("3", 96); + let config = DefaultConfigurationBuilder::new() + .add_struct(vec) + .add_struct(merge) + .build() + .unwrap(); + let value0 = config.get("0"); + assert_eq!(value0.unwrap().as_str(), "32"); + let value1 = config.get("1"); + assert_eq!(value1.unwrap().as_str(), "56"); + let value1 = config.get("3"); + assert_eq!(value1.unwrap().as_str(), "96"); +} + +#[test] +fn load_vec_merge_too_far_and_reify() { + #[derive(Deserialize, Serialize, Clone, Debug)] + #[serde(rename_all(serialize = "PascalCase"))] + #[serde(rename_all(deserialize = "PascalCase"))] + struct ArrayI32 { + values: std::vec::Vec + } + + let opts = ArrayI32 { + values: std::vec::Vec::from([1, 2]), + }; + + let mut merge: HashMap<&str, i32> = HashMap::new(); + merge.insert("Values:3", 96); + + let config = DefaultConfigurationBuilder::new() + .add_struct(opts.clone()) + .add_struct(merge) + .build() + .unwrap(); + let value0 = config.get("Values:0"); + assert_eq!(value0.unwrap().as_str(), "1"); + let value1 = config.get("Values:1"); + assert_eq!(value1.unwrap().as_str(), "2"); + let value1 = config.get("Values:3"); + assert_eq!(value1.unwrap().as_str(), "96"); + + let loaded: ArrayI32 = config.reify(); + assert_eq!(loaded.values[0], 1); + assert_eq!(loaded.values[1], 2); + assert_eq!(loaded.values[2], 96); +} +