diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2021-08-20 09:17:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-20 09:17:22 +0200 |
commit | 0d3a5c3d24a77467366a8d10df439f511dde4dd6 (patch) | |
tree | 70dc9fb2f007ca3dd5df24dda1355501085f1313 | |
parent | 74a0a809f642e2d212752c9ccb767d987c42302e (diff) | |
parent | 9075ca9018417f4b15b60f31e5e3aff64009132c (diff) |
Merge pull request #217 from dlo9/master
Preserve map ordering
48 files changed, 375 insertions, 183 deletions
diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index 23e8047..3ec7270 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -63,6 +63,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --all-features - name: Run cargo test (nightly) if: matrix.rust == '1.46.0' @@ -70,7 +71,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --tests + args: --tests --all-features - name: Run cargo test (nightly) if: matrix.rust == 'nightly' @@ -78,6 +79,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --all-features fmt: needs: [check] @@ -21,6 +21,7 @@ yaml = ["yaml-rust"] hjson = ["serde-hjson"] ini = ["rust-ini"] json5 = ["json5_rs"] +preserve_order = ["indexmap", "toml/preserve_order", "serde_json/preserve_order", "ron/indexmap"] [dependencies] async-trait = "0.1.50" @@ -35,6 +36,7 @@ serde-hjson = { version = "0.9", default-features = false, optional = true } rust-ini = { version = "0.17", optional = true } ron = { version = "0.6", optional = true } json5_rs = { version = "0.3", optional = true, package = "json5" } +indexmap = { version = "1.7.0", features = ["serde-1"], optional = true} [dev-dependencies] serde_derive = "1.0.8" diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs index 10befe0..005a473 100644 --- a/examples/async_source/main.rs +++ b/examples/async_source/main.rs @@ -1,6 +1,6 @@ -use std::{collections::HashMap, error::Error}; +use std::error::Error; -use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat}; +use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Map}; use async_trait::async_trait; use futures::{select, FutureExt}; @@ -56,7 +56,7 @@ struct HttpSource { #[async_trait] impl AsyncSource for HttpSource { - async fn collect(&self) -> Result<HashMap<String, config::Value>, ConfigError> { + async fn collect(&self) -> Result<Map<String, config::Value>, ConfigError> { reqwest::get(&self.uri) .await .map_err(|e| ConfigError::Foreign(Box::new(e)))? // error conversion is possible from custom AsyncSource impls diff --git a/examples/glob/src/main.rs b/examples/glob/src/main.rs index b3183ef..002ddd4 100644 --- a/examples/glob/src/main.rs +++ b/examples/glob/src/main.rs @@ -1,5 +1,5 @@ use std::path::Path; -use std::collections::HashMap; +use std::collections::Map; use config::*; use glob::glob; @@ -14,9 +14,9 @@ fn main() { .merge(File::from(Path::new("conf/05-some.yml"))).unwrap() .merge(File::from(Path::new("conf/99-extra.json"))).unwrap(); - // Print out our settings (as a HashMap) + // Print out our settings (as a Map) println!("\n{:?} \n\n-----------", - settings.try_into::<HashMap<String, String>>().unwrap()); + settings.try_into::<Map<String, String>>().unwrap()); // Option 2 // -------- @@ -28,9 +28,9 @@ fn main() { File::from(Path::new("conf/99-extra.json"))]) .unwrap(); - // Print out our settings (as a HashMap) + // Print out our settings (as a Map) println!("\n{:?} \n\n-----------", - settings.try_into::<HashMap<String, String>>().unwrap()); + settings.try_into::<Map<String, String>>().unwrap()); // Option 3 // -------- @@ -43,7 +43,7 @@ fn main() { .collect::<Vec<_>>()) .unwrap(); - // Print out our settings (as a HashMap) + // Print out our settings (as a Map) println!("\n{:?} \n\n-----------", - settings.try_into::<HashMap<String, String>>().unwrap()); + settings.try_into::<Map<String, String>>().unwrap()); } diff --git a/examples/simple/src/main.rs b/examples/simple/src/main.rs index 1c7ddb7..e07eff2 100644 --- a/examples/simple/src/main.rs +++ b/examples/simple/src/main.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::Map; fn main() { let mut settings = config::Config::default(); @@ -9,7 +9,7 @@ fn main() { // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key .merge(config::Environment::with_prefix("APP")).unwrap(); - // Print out our settings (as a HashMap) + // Print out our settings (as a Map) println!("{:?}", - settings.try_into::<HashMap<String, String>>().unwrap()); + settings.try_into::<Map<String, String>>().unwrap()); } diff --git a/examples/watch/src/main.rs b/examples/watch/src/main.rs index a197390..cbe30e7 100644 --- a/examples/watch/src/main.rs +++ b/examples/watch/src/main.rs @@ -1,5 +1,5 @@ use config::*; -use std::collections::HashMap; +use std::collections::Map; use std::sync::RwLock; use notify::{RecommendedWatcher, DebouncedEvent, Watcher, RecursiveMode}; use std::sync::mpsc::channel; @@ -20,7 +20,7 @@ fn show() { .read() .unwrap() .clone() - .try_into::<HashMap<String, String>>() + .try_into::<Map<String, String>>() .unwrap()); } diff --git a/src/builder.rs b/src/builder.rs index bb88f44..e629606 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,7 +1,8 @@ +use std::iter::IntoIterator; use std::str::FromStr; -use std::{collections::HashMap, iter::IntoIterator}; use crate::error::Result; +use crate::map::Map; use crate::source::AsyncSource; use crate::{config::Config, path::Expression, source::Source, value::Value}; @@ -87,8 +88,8 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// ``` #[derive(Debug, Clone, Default)] pub struct ConfigBuilder<St: BuilderState> { - defaults: HashMap<Expression, Value>, - overrides: HashMap<Expression, Value>, + defaults: Map<Expression, Value>, + overrides: Map<Expression, Value>, state: St, } @@ -120,8 +121,8 @@ pub struct DefaultState { /// Refer to [`ConfigBuilder`] for similar API sample usage or to the examples folder of the crate, where such a source is implemented. #[derive(Debug, Clone, Default)] pub struct AsyncConfigBuilder { - defaults: HashMap<Expression, Value>, - overrides: HashMap<Expression, Value>, + defaults: Map<Expression, Value>, + overrides: Map<Expression, Value>, sources: Vec<SourceType>, } @@ -244,11 +245,11 @@ impl ConfigBuilder<DefaultState> { } fn build_internal( - defaults: HashMap<Expression, Value>, - overrides: HashMap<Expression, Value>, + defaults: Map<Expression, Value>, + overrides: Map<Expression, Value>, sources: &[Box<dyn Source + Send + Sync>], ) -> Result<Config> { - let mut cache: Value = HashMap::<String, Value>::new().into(); + let mut cache: Value = Map::<String, Value>::new().into(); // Add defaults for (key, val) in defaults.into_iter() { @@ -322,11 +323,11 @@ impl ConfigBuilder<AsyncState> { } async fn build_internal( - defaults: HashMap<Expression, Value>, - overrides: HashMap<Expression, Value>, + defaults: Map<Expression, Value>, + overrides: Map<Expression, Value>, sources: &[SourceType], ) -> Result<Config> { - let mut cache: Value = HashMap::<String, Value>::new().into(); + let mut cache: Value = Map::<String, Value>::new().into(); // Add defaults for (key, val) in defaults.into_iter() { diff --git a/src/config.rs b/src/config.rs index b9c64b4..55844ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::fmt::Debug; use crate::builder::{ConfigBuilder, DefaultState}; @@ -6,6 +5,7 @@ use serde::de::Deserialize; use serde::ser::Serialize; use crate::error::*; +use crate::map::Map; use crate::path; use crate::ser::ConfigSerializer; use crate::source::Source; @@ -16,8 +16,8 @@ use crate::value::{Table, Value}; /// them according to the source's priority. #[derive(Clone, Debug)] pub struct Config { - defaults: HashMap<path::Expression, Value>, - overrides: HashMap<path::Expression, Value>, + defaults: Map<path::Expression, Value>, + overrides: Map<path::Expression, Value>, sources: Vec<Box<dyn Source + Send + Sync>>, /// Root of the cached configuration. @@ -83,7 +83,7 @@ impl Config { #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn refresh(&mut self) -> Result<&mut Config> { self.cache = { - let mut cache: Value = HashMap::<String, Value>::new().into(); + let mut cache: Value = Map::<String, Value>::new().into(); // Add defaults for (key, val) in self.defaults.iter() { @@ -181,7 +181,7 @@ impl Config { self.get(key).and_then(Value::into_bool) } - pub fn get_table(&self, key: &str) -> Result<HashMap<String, Value>> { + pub fn get_table(&self, key: &str) -> Result<Map<String, Value>> { self.get(key).and_then(Value::into_table) } @@ -212,7 +212,7 @@ impl Source for Config { Box::new((*self).clone()) } - fn collect(&self) -> Result<HashMap<String, Value>> { + fn collect(&self) -> Result<Map<String, Value>> { self.cache.clone().into_table() } } @@ -1,10 +1,11 @@ -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use std::iter::Enumerate; use serde::de; use crate::config::Config; use crate::error::*; +use crate::map::Map; use crate::value::{Table, Value, ValueKind}; impl<'de> de::Deserializer<'de> for Value { @@ -199,7 +200,7 @@ struct MapAccess { } impl MapAccess { - fn new(table: HashMap<String, Value>) -> Self { + fn new(table: Map<String, Value>) -> Self { MapAccess { elements: table.into_iter().collect(), } @@ -1,7 +1,7 @@ -use std::collections::HashMap; use std::env; use crate::error::*; +use crate::map::Map; use crate::source::Source; use crate::value::{Value, ValueKind}; @@ -79,8 +79,8 @@ impl Source for Environment { Box::new((*self).clone()) } - fn collect(&self) -> Result<HashMap<String, Value>> { - let mut m = HashMap::new(); + fn collect(&self) -> Result<Map<String, Value>> { + let mut m = Map::new(); let uri: String = "the environment".into(); let separator = self.separator.as_deref().unwrap_or(""); diff --git a/src/file/format/hjson.rs b/src/file/format/hjson.rs index f94b1d3..4b76114 100644 --- a/src/file/format/hjson.rs +++ b/src/file/format/hjson.rs @@ -1,19 +1,19 @@ -use std::collections::HashMap; use std::error::Error; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { // Parse a JSON object value from the text // TODO: Have a proper error fire if the root of a file is ever not a Table let value = from_hjson_value(uri, &serde_hjson::from_str(text)?); match value.kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), } } @@ -30,7 +30,7 @@ fn from_hjson_value(uri: Option<&String>, value: &serde_hjson::Value) -> Value { serde_hjson::Value::Bool(value) => Value::new(uri, ValueKind::Boolean(value)), serde_hjson::Value::Object(ref table) => { - let mut m = HashMap::new(); + let mut m = Map::new(); for (key, value) in table { m.insert(key.clone(), from_hjson_value(uri, value)); diff --git a/src/file/format/ini.rs b/src/file/format/ini.rs index b45695a..9295e60 100644 --- a/src/file/format/ini.rs +++ b/src/file/format/ini.rs @@ -1,20 +1,20 @@ -use std::collections::HashMap; use std::error::Error; use ini::Ini; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { - let mut map: HashMap<String, Value> = HashMap::new(); +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { + let mut map: Map<String, Value> = Map::new(); let i = Ini::load_from_str(text)?; for (sec, prop) in i.iter() { match sec { Some(sec) => { - let mut sec_map: HashMap<String, Value> = HashMap::new(); + let mut sec_map: Map<String, Value> = Map::new(); for (k, v) in prop.iter() { sec_map.insert( k.to_owned(), diff --git a/src/file/format/json.rs b/src/file/format/json.rs index 87a6e61..e3d8b87 100644 --- a/src/file/format/json.rs +++ b/src/file/format/json.rs @@ -1,19 +1,19 @@ -use std::collections::HashMap; use std::error::Error; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { // Parse a JSON object value from the text // TODO: Have a proper error fire if the root of a file is ever not a Table let value = from_json_value(uri, &serde_json::from_str(text)?); match value.kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), } } @@ -34,7 +34,7 @@ fn from_json_value(uri: Option<&String>, value: &serde_json::Value) -> Value { serde_json::Value::Bool(value) => Value::new(uri, ValueKind::Boolean(value)), serde_json::Value::Object(ref table) => { - let mut m = HashMap::new(); + let mut m = Map::new(); for (key, value) in table { m.insert(key.clone(), from_json_value(uri, value)); diff --git a/src/file/format/json5.rs b/src/file/format/json5.rs index ea3390f..c156797 100644 --- a/src/file/format/json5.rs +++ b/src/file/format/json5.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use std::error::Error; use crate::error::{ConfigError, Unexpected}; +use crate::map::Map; use crate::value::{Value, ValueKind}; #[derive(serde::Deserialize, Debug)] @@ -13,13 +13,13 @@ pub enum Val { Float(f64), String(String), Array(Vec<Val>), - Object(HashMap<String, Val>), + Object(Map<String, Val>), } pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { match json5_rs::from_str::<Val>(text)? { Val::String(ref value) => Err(Unexpected::Str(value.clone())), Val::Integer(value) => Err(Unexpected::Integer(value)), @@ -29,7 +29,7 @@ pub fn parse( Val::Null => Err(Unexpected::Unit), Val::Object(o) => match from_json5_value(uri, Val::Object(o)).kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), }, } .map_err(|err| ConfigError::invalid_root(uri, err)) diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs index 53bacf6..9f6d7d7 100644 --- a/src/file/format/mod.rs +++ b/src/file/format/mod.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::error::Error; +use crate::map::Map; use crate::value::Value; #[cfg(feature = "toml")] @@ -107,7 +108,7 @@ impl FileFormat { self, uri: Option<&String>, text: &str, - ) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { + ) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { match self { #[cfg(feature = "toml")] FileFormat::Toml => toml::parse(uri, text), diff --git a/src/file/format/ron.rs b/src/file/format/ron.rs index f7dbce3..3fb2a0f 100644 --- a/src/file/format/ron.rs +++ b/src/file/format/ron.rs @@ -1,17 +1,17 @@ -use std::collections::HashMap; use std::error::Error; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { let value = from_ron_value(uri, ron::from_str(text)?)?; match value.kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), } } @@ -56,7 +56,7 @@ fn from_ron_value( Ok((key, value)) }) - .collect::<Result<HashMap<_, _>, _>>()?; + .collect::<Result<Map<_, _>, _>>()?; ValueKind::Table(map) } diff --git a/src/file/format/toml.rs b/src/file/format/toml.rs index c7c5972..af21fc7 100644 --- a/src/file/format/toml.rs +++ b/src/file/format/toml.rs @@ -1,19 +1,19 @@ -use std::collections::HashMap; use std::error::Error; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { // Parse a TOML value from the provided text // TODO: Have a proper error fire if the root of a file is ever not a Table let value = from_toml_value(uri, &toml::from_str(text)?); match value.kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), } } @@ -25,7 +25,7 @@ fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value { toml::Value::Boolean(value) => Value::new(uri, value), toml::Value::Table(ref table) => { - let mut m = HashMap::new(); + let mut m = Map::new(); for (key, value) in table { m.insert(key.clone(), from_toml_value(uri, value)); diff --git a/src/file/format/yaml.rs b/src/file/format/yaml.rs index 2526395..44b72fb 100644 --- a/src/file/format/yaml.rs +++ b/src/file/format/yaml.rs @@ -1,16 +1,16 @@ -use std::collections::HashMap; use std::error::Error; use std::fmt; use std::mem; use yaml_rust as yaml; +use crate::map::Map; use crate::value::{Value, ValueKind}; pub fn parse( uri: Option<&String>, text: &str, -) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> { +) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> { // Parse a YAML object from file let mut docs = yaml::YamlLoader::load_from_str(text)?; let root = match docs.len() { @@ -26,7 +26,7 @@ pub fn parse( match value.kind { ValueKind::Table(map) => Ok(map), - _ => Ok(HashMap::new()), + _ => Ok(Map::new()), } } @@ -40,7 +40,7 @@ fn from_yaml_value(uri: Option<&String>, value: &yaml::Yaml) -> Value { yaml::Yaml::Integer(value) => Value::new(uri, ValueKind::Integer(value)), yaml::Yaml::Boolean(value) => Value::new(uri, ValueKind::Boolean(value)), yaml::Yaml::Hash(ref table) => { - let mut m = HashMap::new(); + let mut m = Map::new(); for (key, value) in table { if let Some(k) = key.as_str() { m.insert(k.to_owned(), from_yaml_value(uri, value)); diff --git a/src/file/mod.rs b/src/file/mod.rs index b00a271..0160d7c 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,10 +1,10 @@ mod format; pub mod source; -use std::collections::HashMap; use std::path::{Path, PathBuf}; use crate::error::*; +use crate::map::Map; use crate::source::Source; use crate::value::Value; @@ -99,7 +99,7 @@ where Box::new((*self).clone()) } - fn collect(&self) -> Result<HashMap<String, Value>> { + fn collect(&self) -> Result<Map<String, Value>> { // Coerce the file contents to a string let (uri, contents, format) = match self .source @@ -110,7 +110,7 @@ where Err(error) => { if !self.required { - return Ok(HashMap::new()); + return Ok(Map::new()); } return Err(error); @@ -59,6 +59,7 @@ mod de; mod env; mod error; mod file; +mod map; mod path; mod ser; mod source; @@ -70,6 +71,7 @@ pub use crate::config::Config; pub use crate::env::Environment; pub use crate::error::ConfigError; pub use crate::file::{File, FileFormat, FileSourceFile, FileSourceString}; +pub use crate::map::Map; pub use crate::source::AsyncSource; pub use crate::source::Source; pub use crate::value::Value; diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..5873f0d --- /dev/null +++ b/src/map.rs @@ -0,0 +1,4 @@ +#[cfg(not(feature = "preserve_order"))] +pub type Map<K, V> = std::collections::HashMap<K, V>; +#[cfg(feature = "preserve_order")] +pub type Map<K, V> = indexmap::IndexMap<K, V>; diff --git a/src/path/mod.rs b/src/path/mod.rs index d58a6f2..cd7ccd7 100644 --- a/src/path/mod.rs +++ b/src/path/mod.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use std::str::FromStr; use crate::error::*; +use crate::map::Map; use crate::value::{Value, ValueKind}; mod parser; @@ -135,7 +135,7 @@ impl Expression { ), _ => { - *value = HashMap::<String, Value>::new().into(); + *value = Map::<String, Value>::new().into(); if let ValueKind::Table(ref mut map) = value.kind { Some( |