diff --git a/Cargo.toml b/Cargo.toml index 79a3fc85..b3dfe7a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,40 +14,19 @@ edition = "2018" [badges] maintenance = { status = "actively-developed" } -[features] -default = ["toml", "json", "yaml", "ini", "ron", "json5"] -json = ["serde_json"] -yaml = ["yaml-rust"] -ini = ["rust-ini"] -json5 = ["json5_rs"] -preserve_order = ["indexmap", "toml/preserve_order", "serde_json/preserve_order", "ron/indexmap"] - [dependencies] -async-trait = "0.1.50" -lazy_static = "1.0" -serde = "1.0.8" -nom = "7" +serde = { version = "1.0.8", features = ["derive"] } +thiserror = "1" +url = "2.2" -toml = { version = "0.5", optional = true } +async-trait = { version = "0.1", optional = true } +itertools = { version = "0.10", optional = true } +futures = { version = "0.3", optional = true } serde_json = { version = "1.0.2", optional = true } -yaml-rust = { version = "0.4", optional = true } -rust-ini = { version = "0.18", optional = true } -ron = { version = "0.7", optional = true } -json5_rs = { version = "0.4", optional = true, package = "json5" } -indexmap = { version = "1.7.0", features = ["serde-1"], optional = true} -pathdiff = "0.2" - -[dev-dependencies] -serde_derive = "1.0.8" -float-cmp = "0.9" -chrono = { version = "0.4", features = ["serde"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]} -warp = "=0.3.1" -futures = "0.3.15" -reqwest = "0.11.10" +toml = { version = "0.5", optional = true } -serde = "1.0" -glob = "0.3" -lazy_static = "1" -notify = "^4.0.0" -temp-env = "0.2.0" +[features] +default = ["json", "toml"] +json = ["serde_json"] +toml = ["dep:toml"] +async = ["async-trait", "futures", "itertools"] diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs deleted file mode 100644 index f5459b91..00000000 --- a/examples/async_source/main.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::{error::Error, fmt::Debug}; - -use config::{ - builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Format, Map, -}; - -use async_trait::async_trait; -use futures::{select, FutureExt}; -use warp::Filter; - -// Example below presents sample configuration server and client. -// -// Server serves simple configuration on HTTP endpoint. -// Client consumes it using custom HTTP AsyncSource built on top of reqwest. - -#[tokio::main] -async fn main() -> Result<(), Box> { - select! { - r = run_server().fuse() => r, - r = run_client().fuse() => r - } -} - -async fn run_server() -> Result<(), Box> { - let service = warp::path("configuration").map(|| r#"{ "value" : 123 }"#); - - println!("Running server on localhost:5001"); - - warp::serve(service).bind(([127, 0, 0, 1], 5001)).await; - - Ok(()) -} - -async fn run_client() -> Result<(), Box> { - // Good enough for an example to allow server to start - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - - let config = ConfigBuilder::::default() - .add_async_source(HttpSource { - uri: "http://localhost:5001/configuration".into(), - format: FileFormat::Json, - }) - .build() - .await?; - - println!("Config value is {}", config.get::("value")?); - - Ok(()) -} - -// Actual implementation of AsyncSource can be found below - -#[derive(Debug)] -struct HttpSource { - uri: String, - format: F, -} - -#[async_trait] -impl AsyncSource for HttpSource { - async fn collect(&self) -> Result, ConfigError> { - reqwest::get(&self.uri) - .await - .map_err(|e| ConfigError::Foreign(Box::new(e)))? // error conversion is possible from custom AsyncSource impls - .text() - .await - .map_err(|e| ConfigError::Foreign(Box::new(e))) - .and_then(|text| { - self.format - .parse(Some(&self.uri), &text) - .map_err(|e| ConfigError::Foreign(e)) - }) - } -} diff --git a/examples/custom_format/main.rs b/examples/custom_format/main.rs deleted file mode 100644 index 4da2e9dc..00000000 --- a/examples/custom_format/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -use config::{Config, File, FileStoredFormat, Format, Map, Value, ValueKind}; - -fn main() { - let config = Config::builder() - .add_source(File::from_str("bad", MyFormat)) - .add_source(File::from_str("good", MyFormat)) - .build(); - - match config { - Ok(cfg) => println!("A config: {:#?}", cfg), - Err(e) => println!("An error: {}", e), - } -} - -#[derive(Debug, Clone)] -pub struct MyFormat; - -impl Format for MyFormat { - fn parse( - &self, - uri: Option<&String>, - text: &str, - ) -> Result, Box> { - // Let's assume our format is somewhat malformed, but this is fine - // In real life anything can be used here - nom, serde or other. - // - // For some more real-life examples refer to format implementation within the library code - let mut result = Map::new(); - - if text == "good" { - result.insert( - "key".to_string(), - Value::new(uri, ValueKind::String(text.into())), - ); - } else { - println!("Something went wrong in {:?}", uri); - } - - Ok(result) - } -} - -// As strange as it seems for config sourced from a string, legacy demands its sacrifice -// It is only required for File source, custom sources can use Format without caring for extensions -static MY_FORMAT_EXT: Vec<&'static str> = vec![]; -impl FileStoredFormat for MyFormat { - fn file_extensions(&self) -> &'static [&'static str] { - &MY_FORMAT_EXT - } -} diff --git a/examples/env-list/main.rs b/examples/env-list/main.rs deleted file mode 100644 index f567419b..00000000 --- a/examples/env-list/main.rs +++ /dev/null @@ -1,25 +0,0 @@ -use config::Config; -#[derive(Debug, Default, serde_derive::Deserialize, PartialEq)] -struct AppConfig { - list: Vec, -} - -fn main() { - std::env::set_var("APP_LIST", "Hello World"); - - let config = Config::builder() - .add_source( - config::Environment::with_prefix("APP") - .try_parsing(true) - .separator("_") - .list_separator(" "), - ) - .build() - .unwrap(); - - let app: AppConfig = config.try_deserialize().unwrap(); - - assert_eq!(app.list, vec![String::from("Hello"), String::from("World")]); - - std::env::remove_var("APP_LIST"); -} diff --git a/examples/glob/conf/00-default.toml b/examples/glob/conf/00-default.toml deleted file mode 100644 index 7b95e7a8..00000000 --- a/examples/glob/conf/00-default.toml +++ /dev/null @@ -1 +0,0 @@ -debug = false diff --git a/examples/glob/conf/05-some.yml b/examples/glob/conf/05-some.yml deleted file mode 100644 index 52555a0c..00000000 --- a/examples/glob/conf/05-some.yml +++ /dev/null @@ -1,2 +0,0 @@ -secret: THIS IS SECRET -debug: true diff --git a/examples/glob/conf/99-extra.json b/examples/glob/conf/99-extra.json deleted file mode 100644 index 26f908c4..00000000 --- a/examples/glob/conf/99-extra.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "that": 3, - "this": 1230, - "key": "sdgnjklsdjklgds" -} diff --git a/examples/glob/main.rs b/examples/glob/main.rs deleted file mode 100644 index d5225a00..00000000 --- a/examples/glob/main.rs +++ /dev/null @@ -1,66 +0,0 @@ -use config::{Config, File}; -use glob::glob; -use std::collections::HashMap; -use std::path::Path; - -fn main() { - // Option 1 - // -------- - // Gather all conf files from conf/ manually - let settings = Config::builder() - // File::with_name(..) is shorthand for File::from(Path::new(..)) - .add_source(File::with_name("examples/glob/conf/00-default.toml")) - .add_source(File::from(Path::new("examples/glob/conf/05-some.yml"))) - .add_source(File::from(Path::new("examples/glob/conf/99-extra.json"))) - .build() - .unwrap(); - - // Print out our settings (as a HashMap) - println!( - "\n{:?} \n\n-----------", - settings - .try_deserialize::>() - .unwrap() - ); - - // Option 2 - // -------- - // Gather all conf files from conf/ manually, but put in 1 merge call. - let settings = Config::builder() - .add_source(vec![ - File::with_name("examples/glob/conf/00-default.toml"), - File::from(Path::new("examples/glob/conf/05-some.yml")), - File::from(Path::new("examples/glob/conf/99-extra.json")), - ]) - .build() - .unwrap(); - - // Print out our settings (as a HashMap) - println!( - "\n{:?} \n\n-----------", - settings - .try_deserialize::>() - .unwrap() - ); - - // Option 3 - // -------- - // Gather all conf files from conf/ using glob and put in 1 merge call. - let settings = Config::builder() - .add_source( - glob("examples/glob/conf/*") - .unwrap() - .map(|path| File::from(path.unwrap())) - .collect::>(), - ) - .build() - .unwrap(); - - // Print out our settings (as a HashMap) - println!( - "\n{:?} \n\n-----------", - settings - .try_deserialize::>() - .unwrap() - ); -} diff --git a/examples/global/main.rs b/examples/global/main.rs deleted file mode 100644 index 160d8817..00000000 --- a/examples/global/main.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![allow(deprecated)] -use config::Config; -use lazy_static::lazy_static; -use std::error::Error; -use std::sync::RwLock; - -lazy_static! { - static ref SETTINGS: RwLock = RwLock::new(Config::default()); -} - -fn try_main() -> Result<(), Box> { - // Set property - SETTINGS.write()?.set("property", 42)?; - - // Get property - println!("property: {}", SETTINGS.read()?.get::("property")?); - - Ok(()) -} - -fn main() { - try_main().unwrap(); -} diff --git a/examples/hierarchical-env/config/default.toml b/examples/hierarchical-env/config/default.toml deleted file mode 100644 index dbb2f302..00000000 --- a/examples/hierarchical-env/config/default.toml +++ /dev/null @@ -1,17 +0,0 @@ -[database] -url = "postgres://postgres@localhost" - -[sparkpost] -key = "sparkpost-dev-key" -token = "sparkpost-dev-token" -url = "https://api.sparkpost.com" -version = 1 - -[twitter] -consumer_token = "twitter-dev-consumer-key" -consumer_secret = "twitter-dev-consumer-secret" - -[braintree] -merchant_id = "braintree-merchant-id" -public_key = "braintree-dev-public-key" -private_key = "braintree-dev-private-key" diff --git a/examples/hierarchical-env/config/development.toml b/examples/hierarchical-env/config/development.toml deleted file mode 100644 index f07dcc2b..00000000 --- a/examples/hierarchical-env/config/development.toml +++ /dev/null @@ -1,4 +0,0 @@ -debug = true - -[database] -echo = true diff --git a/examples/hierarchical-env/config/production.toml b/examples/hierarchical-env/config/production.toml deleted file mode 100644 index cd0c4cf4..00000000 --- a/examples/hierarchical-env/config/production.toml +++ /dev/null @@ -1,13 +0,0 @@ -debug = false - -[sparkpost] -key = "sparkpost-prod-key" -token = "sparkpost-prod-token" - -[twitter] -consumer_token = "twitter-prod-consumer-key" -consumer_secret = "twitter-prod-consumer-secret" - -[braintree] -public_key = "braintree-prod-public-key" -private_key = "braintree-prod-private-key" diff --git a/examples/hierarchical-env/main.rs b/examples/hierarchical-env/main.rs deleted file mode 100644 index ee1b69bd..00000000 --- a/examples/hierarchical-env/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod settings; - -use settings::Settings; - -fn main() { - let settings = Settings::new(); - - // Print out our settings - println!("{:?}", settings); -} diff --git a/examples/hierarchical-env/settings.rs b/examples/hierarchical-env/settings.rs deleted file mode 100644 index 65b5f875..00000000 --- a/examples/hierarchical-env/settings.rs +++ /dev/null @@ -1,76 +0,0 @@ -use config::{Config, ConfigError, Environment, File}; -use serde_derive::Deserialize; -use std::env; - -#[derive(Debug, Deserialize)] -#[allow(unused)] -struct Database { - url: String, -} - -#[derive(Debug, Deserialize)] -#[allow(unused)] -struct Sparkpost { - key: String, - token: String, - url: String, - version: u8, -} - -#[derive(Debug, Deserialize)] -#[allow(unused)] -struct Twitter { - consumer_token: String, - consumer_secret: String, -} - -#[derive(Debug, Deserialize)] -#[allow(unused)] -struct Braintree { - merchant_id: String, - public_key: String, - private_key: String, -} - -#[derive(Debug, Deserialize)] -#[allow(unused)] -pub struct Settings { - debug: bool, - database: Database, - sparkpost: Sparkpost, - twitter: Twitter, - braintree: Braintree, -} - -impl Settings { - pub fn new() -> Result { - let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); - - let s = Config::builder() - // Start off by merging in the "default" configuration file - .add_source(File::with_name("examples/hierarchical-env/config/default")) - // Add in the current environment file - // Default to 'development' env - // Note that this file is _optional_ - .add_source( - File::with_name(&format!("examples/hierarchical-env/config/{}", run_mode)) - .required(false), - ) - // Add in a local configuration file - // This file shouldn't be checked in to git - .add_source(File::with_name("examples/hierarchical-env/config/local").required(false)) - // Add in settings from the environment (with a prefix of APP) - // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key - .add_source(Environment::with_prefix("app")) - // You may also programmatically change settings - .set_override("database.url", "postgres://")? - .build()?; - - // Now that we're done, let's access our configuration - println!("debug: {:?}", s.get_bool("debug")); - println!("database: {:?}", s.get::("database.url")); - - // You can deserialize (and thus freeze) the entire configuration as - s.try_deserialize() - } -} diff --git a/examples/simple/Settings.toml b/examples/simple/Settings.toml deleted file mode 100644 index fd6d3c6d..00000000 --- a/examples/simple/Settings.toml +++ /dev/null @@ -1,3 +0,0 @@ -debug = false -priority = 32 -key = "189rjfadoisfj8923fjio" diff --git a/examples/simple/main.rs b/examples/simple/main.rs deleted file mode 100644 index 55ae8acf..00000000 --- a/examples/simple/main.rs +++ /dev/null @@ -1,21 +0,0 @@ -use config::Config; -use std::collections::HashMap; - -fn main() { - let settings = Config::builder() - // Add in `./Settings.toml` - .add_source(config::File::with_name("examples/simple/Settings")) - // Add in settings from the environment (with a prefix of APP) - // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key - .add_source(config::Environment::with_prefix("APP")) - .build() - .unwrap(); - - // Print out our settings (as a HashMap) - println!( - "{:?}", - settings - .try_deserialize::>() - .unwrap() - ); -} diff --git a/examples/watch/Settings.toml b/examples/watch/Settings.toml deleted file mode 100644 index 1518068c..00000000 --- a/examples/watch/Settings.toml +++ /dev/null @@ -1,3 +0,0 @@ -debug = false -port = 3223 -host = "0.0.0.0" diff --git a/examples/watch/main.rs b/examples/watch/main.rs deleted file mode 100644 index 801ee4fd..00000000 --- a/examples/watch/main.rs +++ /dev/null @@ -1,70 +0,0 @@ -#![allow(deprecated)] -use config::{Config, File}; -use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; -use std::collections::HashMap; -use std::sync::mpsc::channel; -use std::sync::RwLock; -use std::time::Duration; - -lazy_static::lazy_static! { - static ref SETTINGS: RwLock = RwLock::new({ - let mut settings = Config::default(); - settings.merge(File::with_name("examples/watch/Settings.toml")).unwrap(); - - settings - }); -} - -fn show() { - println!( - " * Settings :: \n\x1b[31m{:?}\x1b[0m", - SETTINGS - .read() - .unwrap() - .clone() - .try_deserialize::>() - .unwrap() - ); -} - -fn watch() { - // Create a channel to receive the events. - let (tx, rx) = channel(); - - // Automatically select the best implementation for your platform. - // You can also access each implementation directly e.g. INotifyWatcher. - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2)).unwrap(); - - // Add a path to be watched. All files and directories at that path and - // below will be monitored for changes. - watcher - .watch("examples/watch/Settings.toml", RecursiveMode::NonRecursive) - .unwrap(); - - // This is a simple loop, but you may want to use more complex logic here, - // for example to handle I/O. - loop { - match rx.recv() { - Ok(DebouncedEvent::Write(_)) => { - println!(" * Settings.toml written; refreshing configuration ..."); - SETTINGS.write().unwrap().refresh().unwrap(); - show(); - } - - Err(e) => println!("watch error: {:?}", e), - - _ => { - // Ignore event - } - } - } -} - -fn main() { - // This is just an example of what could be done, today - // We do want this to be built-in to config-rs at some point - // Feel free to take a crack at a PR - - show(); - watch(); -} diff --git a/src/accessor.rs b/src/accessor.rs new file mode 100644 index 00000000..4a6e23b9 --- /dev/null +++ b/src/accessor.rs @@ -0,0 +1,57 @@ +pub trait ParsableAccessor { + fn parse(&self) -> Result; +} + +impl ParsableAccessor for &str { + fn parse(&self) -> Result { + use std::str::FromStr; + + // TODO: Make this non-trivial and bulletproof + + let accessor = self + .split('.') + .map(|s| match usize::from_str(s) { + Ok(u) => AccessType::Index(u), + Err(_) => AccessType::Key(s.to_string()), + }) + .collect(); + + Ok(Accessor::new(accessor)) + } +} + +impl ParsableAccessor for String { + fn parse(&self) -> Result { + let s: &str = self; + ParsableAccessor::parse(&s) + } +} + +pub struct Accessor { + stack: Vec, + index: usize, +} + +impl Accessor { + pub fn new(stack: Vec) -> Self { + Self { stack, index: 0 } + } +} + +pub enum AccessType { + Key(String), + Index(usize), +} + +impl Accessor { + pub(crate) fn current(&self) -> Option<&AccessType> { + self.stack.get(self.index) + } + + pub(crate) fn advance(&mut self) { + self.index += 1; + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AccessorParseError {} diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index 6f928c69..00000000 --- a/src/builder.rs +++ /dev/null @@ -1,370 +0,0 @@ -use std::iter::IntoIterator; -use std::str::FromStr; - -use crate::error::Result; -use crate::map::Map; -use crate::source::AsyncSource; -use crate::{config::Config, path::Expression, source::Source, value::Value}; - -/// A configuration builder -/// -/// It registers ordered sources of configuration to later build consistent [`Config`] from them. -/// Configuration sources it defines are defaults, [`Source`]s and overrides. -/// -/// Defaults are always loaded first and can be overwritten by any of two other sources. -/// Overrides are always loaded last, thus cannot be overridden. -/// Both can be only set explicitly key by key in code -/// using [`set_default`](Self::set_default) or [`set_override`](Self::set_override). -/// -/// An intermediate category, [`Source`], set groups of keys at once implicitly using data coming from external sources -/// like files, environment variables or others that one implements. Defining a [`Source`] is as simple as implementing -/// a trait for a struct. -/// -/// Adding sources, setting defaults and overrides does not invoke any I/O nor builds a config. -/// It happens on demand when [`build`](Self::build) (or its alternative) is called. -/// Therefore all errors, related to any of the [`Source`] will only show up then. -/// -/// # Sync and async builder -/// -/// [`ConfigBuilder`] uses type parameter to keep track of builder state. -/// -/// In [`DefaultState`] builder only supports [`Source`]s -/// -/// In [`AsyncState`] it supports both [`Source`]s and [`AsyncSource`]s at the price of building using `async fn`. -/// -/// # Examples -/// -/// ```rust -/// # use config::*; -/// # use std::error::Error; -/// # fn main() -> Result<(), Box> { -/// let mut builder = Config::builder() -/// .set_default("default", "1")? -/// .add_source(File::new("config/settings", FileFormat::Json)) -/// // .add_async_source(...) -/// .set_override("override", "1")?; -/// -/// match builder.build() { -/// Ok(config) => { -/// // use your config -/// }, -/// Err(e) => { -/// // something went wrong -/// } -/// } -/// # Ok(()) -/// # } -/// ``` -/// -/// If any [`AsyncSource`] is used, the builder will transition to [`AsyncState`]. -/// In such case, it is required to _await_ calls to [`build`](Self::build) and its non-consuming sibling. -/// -/// Calls can be not chained as well -/// ```rust -/// # use std::error::Error; -/// # use config::*; -/// # fn main() -> Result<(), Box> { -/// let mut builder = Config::builder(); -/// builder = builder.set_default("default", "1")?; -/// builder = builder.add_source(File::new("config/settings", FileFormat::Json)); -/// builder = builder.add_source(File::new("config/settings.prod", FileFormat::Json)); -/// builder = builder.set_override("override", "1")?; -/// # Ok(()) -/// # } -/// ``` -/// -/// Calling [`Config::builder`](Config::builder) yields builder in the default state. -/// If having an asynchronous state as the initial state is desired, _turbofish_ notation needs to be used. -/// ```rust -/// # use config::{*, builder::AsyncState}; -/// let mut builder = ConfigBuilder::::default(); -/// ``` -/// -/// If for some reason acquiring builder in default state is required without calling [`Config::builder`](Config::builder) -/// it can also be achieved. -/// ```rust -/// # use config::{*, builder::DefaultState}; -/// let mut builder = ConfigBuilder::::default(); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct ConfigBuilder { - defaults: Map, - overrides: Map, - state: St, -} - -/// Represents [`ConfigBuilder`] state. -pub trait BuilderState {} - -/// Represents data specific to builder in default, sychronous state, without support for async. -#[derive(Debug, Default)] -pub struct DefaultState { - sources: Vec>, -} - -/// The asynchronous configuration builder. -/// -/// Similar to a [`ConfigBuilder`] it maintains a set of defaults, a set of sources, and overrides. -/// -/// Defaults do not override anything, sources override defaults, and overrides override anything else. -/// Within those three groups order of adding them at call site matters - entities added later take precedence. -/// -/// For more detailed description and examples see [`ConfigBuilder`]. -/// [`AsyncConfigBuilder`] is just an extension of it that takes async functions into account. -/// -/// To obtain a [`Config`] call [`build`](AsyncConfigBuilder::build) or [`build_cloned`](AsyncConfigBuilder::build_cloned) -/// -/// # Example -/// Since this library does not implement any [`AsyncSource`] an example in rustdocs cannot be given. -/// Detailed explanation about why such a source is not implemented is in [`AsyncSource`]'s documentation. -/// -/// 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 {} - -/// Represents data specific to builder in asychronous state, with support for async. -#[derive(Debug, Default)] -pub struct AsyncState { - sources: Vec, -} - -#[derive(Debug, Clone)] -enum SourceType { - Sync(Box), - Async(Box), -} - -impl BuilderState for DefaultState {} -impl BuilderState for AsyncState {} - -impl ConfigBuilder { - // operations allowed in any state - - /// Set a default `value` at `key` - /// - /// This value can be overwritten by any [`Source`], [`AsyncSource`] or override. - /// - /// # Errors - /// - /// Fails if `Expression::from_str(key)` fails. - pub fn set_default(mut self, key: S, value: T) -> Result - where - S: AsRef, - T: Into, - { - self.defaults - .insert(Expression::from_str(key.as_ref())?, value.into()); - Ok(self) - } - - /// Set an override - /// - /// This function sets an overwrite value. It will not be altered by any default, [`Source`] nor [`AsyncSource`] - /// - /// # Errors - /// - /// Fails if `Expression::from_str(key)` fails. - pub fn set_override(mut self, key: S, value: T) -> Result - where - S: AsRef, - T: Into, - { - self.overrides - .insert(Expression::from_str(key.as_ref())?, value.into()); - Ok(self) - } - - /// Sets an override if value is Some(_) - /// - /// This function sets an overwrite value if Some(_) is passed. If None is passed, this function does nothing. - /// It will not be altered by any default, [`Source`] nor [`AsyncSource`] - /// - /// # Errors - /// - /// Fails if `Expression::from_str(key)` fails. - pub fn set_override_option(mut self, key: S, value: Option) -> Result - where - S: AsRef, - T: Into, - { - if let Some(value) = value { - self.overrides - .insert(Expression::from_str(key.as_ref())?, value.into()); - } - Ok(self) - } -} - -impl ConfigBuilder { - // operations allowed in sync state - - /// Registers new [`Source`] in this builder. - /// - /// Calling this method does not invoke any I/O. [`Source`] is only saved in internal register for later use. - #[must_use] - pub fn add_source(mut self, source: T) -> Self - where - T: Source + Send + Sync + 'static, - { - self.state.sources.push(Box::new(source)); - self - } - - /// Registers new [`AsyncSource`] in this builder and forces transition to [`AsyncState`]. - /// - /// Calling this method does not invoke any I/O. [`AsyncSource`] is only saved in internal register for later use. - pub fn add_async_source(self, source: T) -> ConfigBuilder - where - T: AsyncSource + Send + Sync + 'static, - { - let async_state = ConfigBuilder { - state: AsyncState { - sources: self - .state - .sources - .into_iter() - .map(|s| SourceType::Sync(s)) - .collect(), - }, - defaults: self.defaults, - overrides: self.overrides, - }; - - async_state.add_async_source(source) - } - - /// Reads all registered [`Source`]s. - /// - /// This is the method that invokes all I/O operations. - /// For a non consuming alternative see [`build_cloned`](Self::build_cloned) - /// - /// # Errors - /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, - /// this method returns error. - pub fn build(self) -> Result { - Self::build_internal(self.defaults, self.overrides, &self.state.sources) - } - - /// Reads all registered [`Source`]s. - /// - /// Similar to [`build`](Self::build), but it does not take ownership of `ConfigBuilder` to allow later reuse. - /// Internally it clones data to achieve it. - /// - /// # Errors - /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, - /// this method returns error. - pub fn build_cloned(&self) -> Result { - Self::build_internal( - self.defaults.clone(), - self.overrides.clone(), - &self.state.sources, - ) - } - - fn build_internal( - defaults: Map, - overrides: Map, - sources: &[Box], - ) -> Result { - let mut cache: Value = Map::::new().into(); - - // Add defaults - for (key, val) in defaults { - key.set(&mut cache, val); - } - - // Add sources - sources.collect_to(&mut cache)?; - - // Add overrides - for (key, val) in overrides { - key.set(&mut cache, val); - } - - Ok(Config::new(cache)) - } -} - -impl ConfigBuilder { - // operations allowed in async state - - /// Registers new [`Source`] in this builder. - /// - /// Calling this method does not invoke any I/O. [`Source`] is only saved in internal register for later use. - #[must_use] - pub fn add_source(mut self, source: T) -> Self - where - T: Source + Send + Sync + 'static, - { - self.state.sources.push(SourceType::Sync(Box::new(source))); - self - } - - /// Registers new [`AsyncSource`] in this builder. - /// - /// Calling this method does not invoke any I/O. [`AsyncSource`] is only saved in internal register for later use. - #[must_use] - pub fn add_async_source(mut self, source: T) -> Self - where - T: AsyncSource + Send + Sync + 'static, - { - self.state.sources.push(SourceType::Async(Box::new(source))); - self - } - - /// Reads all registered defaults, [`Source`]s, [`AsyncSource`]s and overrides. - /// - /// This is the method that invokes all I/O operations. - /// For a non consuming alternative see [`build_cloned`](Self::build_cloned) - /// - /// # Errors - /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, - /// this method returns error. - pub async fn build(self) -> Result { - Self::build_internal(self.defaults, self.overrides, &self.state.sources).await - } - - /// Reads all registered defaults, [`Source`]s, [`AsyncSource`]s and overrides. - /// - /// Similar to [`build`](Self::build), but it does not take ownership of `ConfigBuilder` to allow later reuse. - /// Internally it clones data to achieve it. - /// - /// # Errors - /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, - /// this method returns error. - pub async fn build_cloned(&self) -> Result { - Self::build_internal( - self.defaults.clone(), - self.overrides.clone(), - &self.state.sources, - ) - .await - } - - async fn build_internal( - defaults: Map, - overrides: Map, - sources: &[SourceType], - ) -> Result { - let mut cache: Value = Map::::new().into(); - - // Add defaults - for (key, val) in defaults { - key.set(&mut cache, val); - } - - for source in sources.iter() { - match source { - SourceType::Sync(source) => source.collect_to(&mut cache)?, - SourceType::Async(source) => source.collect_to(&mut cache).await?, - } - } - - // Add overrides - for (key, val) in overrides { - key.set(&mut cache, val); - } - - Ok(Config::new(cache)) - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 27b318dd..00000000 --- a/src/config.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::fmt::Debug; - -use crate::builder::{ConfigBuilder, DefaultState}; -use serde::de::Deserialize; -use serde::ser::Serialize; - -use crate::error::{ConfigError, Result}; -use crate::map::Map; -use crate::path; -use crate::ser::ConfigSerializer; -use crate::source::Source; -use crate::value::{Table, Value}; - -/// A prioritized configuration repository. It maintains a set of -/// configuration sources, fetches values to populate those, and provides -/// them according to the source's priority. -#[derive(Clone, Debug)] -pub struct Config { - defaults: Map, - overrides: Map, - sources: Vec>, - - /// Root of the cached configuration. - pub cache: Value, -} - -impl Default for Config { - fn default() -> Self { - Self { - defaults: Default::default(), - overrides: Default::default(), - sources: Default::default(), - cache: Value::new(None, Table::new()), - } - } -} - -impl Config { - pub(crate) fn new(value: Value) -> Self { - Self { - cache: value, - ..Self::default() - } - } - - /// Creates new [`ConfigBuilder`] instance - pub fn builder() -> ConfigBuilder { - ConfigBuilder::::default() - } - - /// Merge in a configuration property source. - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn merge(&mut self, source: T) -> Result<&mut Self> - where - T: 'static, - T: Source + Send + Sync, - { - self.sources.push(Box::new(source)); - - #[allow(deprecated)] - self.refresh() - } - - /// Merge in a configuration property source. - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn with_merged(mut self, source: T) -> Result - where - T: 'static, - T: Source + Send + Sync, - { - self.sources.push(Box::new(source)); - - #[allow(deprecated)] - self.refresh()?; - Ok(self) - } - - /// Refresh the configuration cache with fresh - /// data from added sources. - /// - /// Configuration is automatically refreshed after a mutation - /// operation (`set`, `merge`, `set_default`, etc.). - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn refresh(&mut self) -> Result<&mut Self> { - self.cache = { - let mut cache: Value = Map::::new().into(); - - // Add defaults - for (key, val) in &self.defaults { - key.set(&mut cache, val.clone()); - } - - // Add sources - self.sources.collect_to(&mut cache)?; - - // Add overrides - for (key, val) in &self.overrides { - key.set(&mut cache, val.clone()); - } - - cache - }; - - Ok(self) - } - - /// Set a default `value` at `key` - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn set_default(&mut self, key: &str, value: T) -> Result<&mut Self> - where - T: Into, - { - self.defaults.insert(key.parse()?, value.into()); - - #[allow(deprecated)] - self.refresh() - } - - /// Set an overwrite - /// - /// This function sets an overwrite value. - /// The overwrite `value` is written to the `key` location on every `refresh()` - /// - /// # Warning - /// - /// Errors if config is frozen - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn set(&mut self, key: &str, value: T) -> Result<&mut Self> - where - T: Into, - { - self.overrides.insert(key.parse()?, value.into()); - - #[allow(deprecated)] - self.refresh() - } - - #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] - pub fn set_once(&mut self, key: &str, value: Value) -> Result<()> { - let expr: path::Expression = key.parse()?; - - // Traverse the cache using the path to (possibly) retrieve a value - if let Some(ref mut val) = expr.get_mut(&mut self.cache) { - **val = value; - } else { - expr.set(&mut self.cache, value); - } - Ok(()) - } - - pub fn get<'de, T: Deserialize<'de>>(&self, key: &str) -> Result { - // Parse the key into a path expression - let expr: path::Expression = key.parse()?; - - // Traverse the cache using the path to (possibly) retrieve a value - let value = expr.get(&self.cache).cloned(); - - match value { - Some(value) => { - // Deserialize the received value into the requested type - T::deserialize(value).map_err(|e| e.extend_with_key(key)) - } - - None => Err(ConfigError::NotFound(key.into())), - } - } - - pub fn get_string(&self, key: &str) -> Result { - self.get(key).and_then(Value::into_string) - } - - pub fn get_int(&self, key: &str) -> Result { - self.get(key).and_then(Value::into_int) - } - - pub fn get_float(&self, key: &str) -> Result { - self.get(key).and_then(Value::into_float) - } - - pub fn get_bool(&self, key: &str) -> Result { - self.get(key).and_then(Value::into_bool) - } - - pub fn get_table(&self, key: &str) -> Result> { - self.get(key).and_then(Value::into_table) - } - - pub fn get_array(&self, key: &str) -> Result> { - self.get(key).and_then(Value::into_array) - } - - /// Attempt to deserialize the entire configuration into the requested type. - pub fn try_deserialize<'de, T: Deserialize<'de>>(self) -> Result { - T::deserialize(self) - } - - /// Attempt to serialize the entire configuration from the given type. - pub fn try_from(from: &T) -> Result { - let mut serializer = ConfigSerializer::default(); - from.serialize(&mut serializer)?; - Ok(serializer.output) - } - - #[deprecated(since = "0.7.0", note = "please use 'try_deserialize' instead")] - pub fn deserialize<'de, T: Deserialize<'de>>(self) -> Result { - self.try_deserialize() - } -} - -impl Source for Config { - fn clone_into_box(&self) -> Box { - Box::new((*self).clone()) - } - - fn collect(&self) -> Result> { - self.cache.clone().into_table() - } -} diff --git a/src/config/builder.rs b/src/config/builder.rs new file mode 100644 index 00000000..ea5462c7 --- /dev/null +++ b/src/config/builder.rs @@ -0,0 +1,51 @@ +use crate::config::Config; +use crate::object::ConfigObject; +use crate::source::ConfigSource; +use crate::source::SourceError; + +use super::ConfigError; + +#[derive(Debug)] +pub struct ConfigBuilder { + layers_builders: Vec>, + defaults_builders: Vec>, + overwrites_builders: Vec>, +} + +impl ConfigBuilder { + pub(crate) fn new() -> Self { + ConfigBuilder { + layers_builders: Vec::new(), + defaults_builders: Vec::new(), + overwrites_builders: Vec::new(), + } + } + + pub fn load(mut self, source: Box) -> Self { + self.layers_builders.push(source); + self + } + + pub fn load_default(mut self, source: Box) -> Self { + self.defaults_builders.push(source); + self + } + + pub fn load_overwrite(mut self, source: Box) -> Self { + self.overwrites_builders.push(source); + self + } + + pub fn build(&self) -> Result { + Config::build_from_builder(self) + } + + pub(crate) fn reload(&self) -> Result, SourceError> { + self.overwrites_builders + .iter() + .map(|cs| cs.load()) + .chain(self.layers_builders.iter().map(|cs| cs.load())) + .chain(self.defaults_builders.iter().map(|cs| cs.load())) + .collect() + } +} diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 00000000..ca1f93b2 --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,74 @@ +use crate::accessor::Accessor; +use crate::accessor::ParsableAccessor; +use crate::config::ConfigBuilder; +use crate::config::ConfigError; +use crate::element::ConfigElement; +use crate::object::ConfigObject; + +#[derive(Debug)] +pub struct Config { + layers: Vec, +} + +impl Config { + pub fn builder() -> ConfigBuilder { + ConfigBuilder::new() + } + + pub(super) fn build_from_builder(builder: &ConfigBuilder) -> Result { + let config = Config { + layers: builder.reload()?, + }; + + Ok(config) + } + + /// Access the configuration at a specific position + /// + /// Use an object of a type implementing the `ParsableAccessor` trait for accessing the + /// configuration at a certain position. + /// As `ParsableAccessor` is implemented by [`&str`] and [`String`], passing those directly + /// works. + /// + /// # Note + /// + /// Each time, [`Config::get`] is called, the `ParsableAccessor::parse()` function is called. + /// If that is a unbearable overhead (especially in cases where the accessor is hard-coded), + /// [`Config::get_with_accessor`] can be used to prevent that overhead. + /// + /// # Examples + /// + /// ```no_run + /// # use crate::config::Config; + /// let config: Config = { //... + /// # unimplemented!() + /// }; + /// + /// config.get("foo") + /// // ... + /// # ; + /// ``` + pub fn get(&self, accessor: A) -> Result, ConfigError> + where + A: ParsableAccessor, + { + let accessor = accessor.parse()?; + self.get_with_accessor(accessor) + } + + /// Access the configuration at a specific position + /// + /// See [`Config::get`] + pub fn get_with_accessor( + &self, + mut accessor: Accessor, + ) -> Result, ConfigError> { + for layer in self.layers.iter() { + if let Some(value) = layer.get(&mut accessor)? { + return Ok(Some(value)); + } + } + + Ok(None) + } +} diff --git a/src/config/error.rs b/src/config/error.rs new file mode 100644 index 00000000..a82acb11 --- /dev/null +++ b/src/config/error.rs @@ -0,0 +1,22 @@ +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("Accessor parser error")] + AccessorParseError(#[from] crate::accessor::AccessorParseError), + + #[error("Config object access error")] + ConfigObjectAccessError(#[from] crate::object::ConfigObjectAccessError), + + #[error("Error loading Source")] + SourceError(#[from] crate::source::SourceError), + + #[error("RwLock poisoned")] + InternalRwLockPoisioned, + + #[error("Configuration is not loaded")] + NotLoaded, +} + +#[derive(Debug, thiserror::Error)] +pub enum ConfigBuilderError { + Wrapped(E), +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 00000000..133a424e --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,162 @@ +mod builder; +mod config; +mod error; + +pub use crate::config::builder::*; +pub use crate::config::config::*; +pub use crate::config::error::*; + +#[cfg(test)] +mod tests { + use super::*; + use crate::element::ConfigElement; + use crate::element::IntoConfigElement; + + #[test] + fn test_compile_loading() { + let _c = Config::builder() + .load(Box::new(crate::source::test_source::TestSource( + ConfigElement::Null, + ))) + .build() + .unwrap(); + } + + #[test] + #[cfg(feature = "json")] + fn test_load_json() { + let json: serde_json::Value = serde_json::from_str( + r#" + { "key": "value" } + "#, + ) + .unwrap(); + + let _c = Config::builder() + .load(Box::new(crate::source::test_source::TestSource( + json.into_config_element().unwrap(), + ))) + .build() + .unwrap(); + } + + #[test] + #[cfg(feature = "json")] + fn test_load_json_get_value() { + let json: serde_json::Value = serde_json::from_str( + r#" + { "key": "value" } + "#, + ) + .unwrap(); + + let source = crate::source::test_source::TestSource(json.into_config_element().unwrap()); + + let c = Config::builder().load(Box::new(source)).build().unwrap(); + + let r = c.get("key"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value"), + _ => panic!(), + } + } + + #[test] + #[cfg(feature = "json")] + fn test_layered_json_config() { + let json1: serde_json::Value = serde_json::from_str( + r#" + { "key1": "value1" } + "#, + ) + .unwrap(); + + let json2: serde_json::Value = serde_json::from_str( + r#" + { "key1": "value2", "key2": "value3" } + "#, + ) + .unwrap(); + + let source1 = crate::source::test_source::TestSource(json1.into_config_element().unwrap()); + let source2 = crate::source::test_source::TestSource(json2.into_config_element().unwrap()); + + let c = Config::builder() + .load(Box::new(source1)) + .load(Box::new(source2)) + .build() + .unwrap(); + + let r = c.get("key1"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value1"), + _ => panic!(), + } + + let r = c.get("key2"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value3"), + _ => panic!(), + } + } + + #[test] + #[cfg(all(feature = "json", feature = "toml"))] + fn test_layered_json_toml_config() { + let json: serde_json::Value = serde_json::from_str( + r#" + { "key1": "value1" } + "#, + ) + .unwrap(); + + let toml: toml::Value = toml::from_str( + r#" + key1 = "value2" + key2 = "value3" + "#, + ) + .unwrap(); + + let source1 = crate::source::test_source::TestSource(json.into_config_element().unwrap()); + let source2 = crate::source::test_source::TestSource(toml.into_config_element().unwrap()); + + let c = Config::builder() + .load(Box::new(source1)) + .load(Box::new(source2)) + .build() + .unwrap(); + + let r = c.get("key1"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value1"), + _ => panic!(), + } + + let r = c.get("key2"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value3"), + _ => panic!(), + } + } +} diff --git a/src/de.rs b/src/de.rs deleted file mode 100644 index 9df347f0..00000000 --- a/src/de.rs +++ /dev/null @@ -1,468 +0,0 @@ -use std::collections::VecDeque; -use std::iter::Enumerate; - -use serde::de; - -use crate::config::Config; -use crate::error::{ConfigError, Result}; -use crate::map::Map; -use crate::value::{Table, Value, ValueKind}; - -impl<'de> de::Deserializer<'de> for Value { - type Error = ConfigError; - - #[inline] - fn deserialize_any(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - // Deserialize based on the underlying type - match self.kind { - ValueKind::Nil => visitor.visit_unit(), - ValueKind::I64(i) => visitor.visit_i64(i), - ValueKind::I128(i) => visitor.visit_i128(i), - ValueKind::U64(i) => visitor.visit_u64(i), - ValueKind::U128(i) => visitor.visit_u128(i), - ValueKind::Boolean(b) => visitor.visit_bool(b), - ValueKind::Float(f) => visitor.visit_f64(f), - ValueKind::String(s) => visitor.visit_string(s), - ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)), - ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)), - } - } - - #[inline] - fn deserialize_bool>(self, visitor: V) -> Result { - visitor.visit_bool(self.into_bool()?) - } - - #[inline] - fn deserialize_i8>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i8(self.into_int()? as i8) - } - - #[inline] - fn deserialize_i16>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i16(self.into_int()? as i16) - } - - #[inline] - fn deserialize_i32>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i32(self.into_int()? as i32) - } - - #[inline] - fn deserialize_i64>(self, visitor: V) -> Result { - visitor.visit_i64(self.into_int()?) - } - - #[inline] - fn deserialize_u8>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u8(self.into_int()? as u8) - } - - #[inline] - fn deserialize_u16>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u16(self.into_int()? as u16) - } - - #[inline] - fn deserialize_u32>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u32(self.into_int()? as u32) - } - - #[inline] - fn deserialize_u64>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u64(self.into_int()? as u64) - } - - #[inline] - fn deserialize_f32>(self, visitor: V) -> Result { - visitor.visit_f32(self.into_float()? as f32) - } - - #[inline] - fn deserialize_f64>(self, visitor: V) -> Result { - visitor.visit_f64(self.into_float()?) - } - - #[inline] - fn deserialize_str>(self, visitor: V) -> Result { - visitor.visit_string(self.into_string()?) - } - - #[inline] - fn deserialize_string>(self, visitor: V) -> Result { - visitor.visit_string(self.into_string()?) - } - - #[inline] - fn deserialize_option(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - // Match an explicit nil as None and everything else as Some - match self.kind { - ValueKind::Nil => visitor.visit_none(), - _ => visitor.visit_some(self), - } - } - - fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_enum( - self, - name: &'static str, - variants: &'static [&'static str], - visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_enum(EnumAccess { - value: self, - name, - variants, - }) - } - - serde::forward_to_deserialize_any! { - char seq - bytes byte_buf map struct unit - identifier ignored_any unit_struct tuple_struct tuple - } -} - -struct StrDeserializer<'a>(&'a str); - -impl<'de, 'a> de::Deserializer<'de> for StrDeserializer<'a> { - type Error = ConfigError; - - #[inline] - fn deserialize_any>(self, visitor: V) -> Result { - visitor.visit_str(self.0) - } - - serde::forward_to_deserialize_any! { - bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq - bytes byte_buf map struct unit enum newtype_struct - identifier ignored_any unit_struct tuple_struct tuple option - } -} - -struct SeqAccess { - elements: Enumerate<::std::vec::IntoIter>, -} - -impl SeqAccess { - fn new(elements: Vec) -> Self { - Self { - elements: elements.into_iter().enumerate(), - } - } -} - -impl<'de> de::SeqAccess<'de> for SeqAccess { - type Error = ConfigError; - - fn next_element_seed(&mut self, seed: T) -> Result> - where - T: de::DeserializeSeed<'de>, - { - match self.elements.next() { - Some((idx, value)) => seed - .deserialize(value) - .map(Some) - .map_err(|e| e.prepend_index(idx)), - None => Ok(None), - } - } - - fn size_hint(&self) -> Option { - match self.elements.size_hint() { - (lower, Some(upper)) if lower == upper => Some(upper), - _ => None, - } - } -} - -struct MapAccess { - elements: VecDeque<(String, Value)>, -} - -impl MapAccess { - fn new(table: Map) -> Self { - Self { - elements: table.into_iter().collect(), - } - } -} - -impl<'de> de::MapAccess<'de> for MapAccess { - type Error = ConfigError; - - fn next_key_seed(&mut self, seed: K) -> Result> - where - K: de::DeserializeSeed<'de>, - { - if let Some(&(ref key_s, _)) = self.elements.front() { - let key_de = Value::new(None, key_s as &str); - let key = de::DeserializeSeed::deserialize(seed, key_de)?; - - Ok(Some(key)) - } else { - Ok(None) - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: de::DeserializeSeed<'de>, - { - let (key, value) = self.elements.pop_front().unwrap(); - de::DeserializeSeed::deserialize(seed, value).map_err(|e| e.prepend_key(&key)) - } -} - -struct EnumAccess { - value: Value, - name: &'static str, - variants: &'static [&'static str], -} - -impl EnumAccess { - fn variant_deserializer(&self, name: &str) -> Result { - self.variants - .iter() - .find(|&&s| s == name) - .map(|&s| StrDeserializer(s)) - .ok_or_else(|| self.no_constructor_error(name)) - } - - fn table_deserializer(&self, table: &Table) -> Result { - if table.len() == 1 { - self.variant_deserializer(table.iter().next().unwrap().0) - } else { - Err(self.structural_error()) - } - } - - fn no_constructor_error(&self, supposed_variant: &str) -> ConfigError { - ConfigError::Message(format!( - "enum {} does not have variant constructor {}", - self.name, supposed_variant - )) - } - - fn structural_error(&self) -> ConfigError { - ConfigError::Message(format!( - "value of enum {} should be represented by either string or table with exactly one key", - self.name - )) - } -} - -impl<'de> de::EnumAccess<'de> for EnumAccess { - type Error = ConfigError; - type Variant = Self; - - fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant)> - where - V: de::DeserializeSeed<'de>, - { - let value = { - let deserializer = match self.value.kind { - ValueKind::String(ref s) => self.variant_deserializer(s), - ValueKind::Table(ref t) => self.table_deserializer(t), - _ => Err(self.structural_error()), - }?; - seed.deserialize(deserializer)? - }; - - Ok((value, self)) - } -} - -impl<'de> de::VariantAccess<'de> for EnumAccess { - type Error = ConfigError; - - fn unit_variant(self) -> Result<()> { - Ok(()) - } - - fn newtype_variant_seed(self, seed: T) -> Result - where - T: de::DeserializeSeed<'de>, - { - match self.value.kind { - ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1), - _ => unreachable!(), - } - } - - fn tuple_variant(self, _len: usize, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - match self.value.kind { - ValueKind::Table(t) => { - de::Deserializer::deserialize_seq(t.into_iter().next().unwrap().1, visitor) - } - _ => unreachable!(), - } - } - - fn struct_variant(self, _fields: &'static [&'static str], visitor: V) -> Result - where - V: de::Visitor<'de>, - { - match self.value.kind { - ValueKind::Table(t) => { - de::Deserializer::deserialize_map(t.into_iter().next().unwrap().1, visitor) - } - _ => unreachable!(), - } - } -} - -impl<'de> de::Deserializer<'de> for Config { - type Error = ConfigError; - - #[inline] - fn deserialize_any(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - // Deserialize based on the underlying type - match self.cache.kind { - ValueKind::Nil => visitor.visit_unit(), - ValueKind::I64(i) => visitor.visit_i64(i), - ValueKind::I128(i) => visitor.visit_i128(i), - ValueKind::U64(i) => visitor.visit_u64(i), - ValueKind::U128(i) => visitor.visit_u128(i), - ValueKind::Boolean(b) => visitor.visit_bool(b), - ValueKind::Float(f) => visitor.visit_f64(f), - ValueKind::String(s) => visitor.visit_string(s), - ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)), - ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)), - } - } - - #[inline] - fn deserialize_bool>(self, visitor: V) -> Result { - visitor.visit_bool(self.cache.into_bool()?) - } - - #[inline] - fn deserialize_i8>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i8(self.cache.into_int()? as i8) - } - - #[inline] - fn deserialize_i16>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i16(self.cache.into_int()? as i16) - } - - #[inline] - fn deserialize_i32>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i32(self.cache.into_int()? as i32) - } - - #[inline] - fn deserialize_i64>(self, visitor: V) -> Result { - visitor.visit_i64(self.cache.into_int()?) - } - - #[inline] - fn deserialize_u8>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u8(self.cache.into_int()? as u8) - } - - #[inline] - fn deserialize_u16>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u16(self.cache.into_int()? as u16) - } - - #[inline] - fn deserialize_u32>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u32(self.cache.into_int()? as u32) - } - - #[inline] - fn deserialize_u64>(self, visitor: V) -> Result { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u64(self.cache.into_int()? as u64) - } - - #[inline] - fn deserialize_f32>(self, visitor: V) -> Result { - visitor.visit_f32(self.cache.into_float()? as f32) - } - - #[inline] - fn deserialize_f64>(self, visitor: V) -> Result { - visitor.visit_f64(self.cache.into_float()?) - } - - #[inline] - fn deserialize_str>(self, visitor: V) -> Result { - visitor.visit_string(self.cache.into_string()?) - } - - #[inline] - fn deserialize_string>(self, visitor: V) -> Result { - visitor.visit_string(self.cache.into_string()?) - } - - #[inline] - fn deserialize_option(self, visitor: V) -> Result - where - V: de::Visitor<'de>, - { - // Match an explicit nil as None and everything else as Some - match self.cache.kind { - ValueKind::Nil => visitor.visit_none(), - _ => visitor.visit_some(self), - } - } - - fn deserialize_enum( - self, - name: &'static str, - variants: &'static [&'static str], - visitor: V, - ) -> Result - where - V: de::Visitor<'de>, - { - visitor.visit_enum(EnumAccess { - value: self.cache, - name, - variants, - }) - } - - serde::forward_to_deserialize_any! { - char seq - bytes byte_buf map struct unit newtype_struct - identifier ignored_any unit_struct tuple_struct tuple - } -} diff --git a/src/description.rs b/src/description.rs new file mode 100644 index 00000000..0b2a8abd --- /dev/null +++ b/src/description.rs @@ -0,0 +1,10 @@ +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ConfigSourceDescription { + Unknown, + Default, + Overwrite, + Path(std::path::PathBuf), + Uri(url::Url), + Custom(String), +} diff --git a/src/element/json.rs b/src/element/json.rs new file mode 100644 index 00000000..c2636eb0 --- /dev/null +++ b/src/element/json.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use crate::element::ConfigElement; +use crate::element::IntoConfigElement; + +#[derive(Debug, thiserror::Error)] +pub enum JsonIntoConfigElementError {} + +impl IntoConfigElement for serde_json::Value { + type Error = JsonIntoConfigElementError; + + fn into_config_element(self) -> Result { + match self { + serde_json::Value::Null => Ok(ConfigElement::Null), + serde_json::Value::Bool(b) => Ok(ConfigElement::Bool(b)), + serde_json::Value::Number(num) => num + .as_i64() + .map(ConfigElement::I64) + .or_else(|| num.as_u64().map(ConfigElement::U64)) + .or_else(|| num.as_f64().map(ConfigElement::F64)) + .ok_or_else(|| unimplemented!()), + serde_json::Value::String(s) => Ok(ConfigElement::Str(s)), + serde_json::Value::Array(vec) => vec + .into_iter() + .map(|v| v.into_config_element()) + .collect::, Self::Error>>() + .map(ConfigElement::List), + serde_json::Value::Object(obj) => obj + .into_iter() + .map(|(k, v)| v.into_config_element().map(|v| (k.to_string(), v))) + .collect::, JsonIntoConfigElementError>>() + .map(|map| ConfigElement::Map(map)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::element::ConfigElement; + + #[test] + fn deser_json_1() { + let s = r#" + { "key": "value" } + "#; + + let e: ConfigElement = serde_json::from_str(s).unwrap(); + match e { + ConfigElement::Map(map) => { + assert_eq!( + *map.get("key").unwrap(), + ConfigElement::Str("value".to_string()) + ); + } + _ => panic!("Not a map"), + } + } +} diff --git a/src/element/mod.rs b/src/element/mod.rs new file mode 100644 index 00000000..2f13147c --- /dev/null +++ b/src/element/mod.rs @@ -0,0 +1,237 @@ +use std::collections::HashMap; + +use crate::{ + accessor::{AccessType, Accessor}, + object::ConfigObjectAccessError, +}; + +#[derive(Clone, Debug, PartialEq, serde::Deserialize)] +#[serde(untagged)] +pub enum ConfigElement { + Null, + Bool(bool), + I8(i8), + I16(i16), + I32(i32), + I64(i64), + U8(u8), + U16(u16), + U32(u32), + U64(u64), + F32(f32), + F64(f64), + Str(String), + List(Vec), + Map(HashMap), +} + +impl ConfigElement { + pub(crate) fn get( + &self, + accessor: &mut Accessor, + ) -> Result, ConfigObjectAccessError> { + match (accessor.current(), &self) { + (Some(AccessType::Key(k)), ConfigElement::Null) => { + Err(ConfigObjectAccessError::AccessWithKeyOnNull(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::Bool(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnBool(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::I8(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnI8(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::I16(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnI16(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::I32(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnI32(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::I64(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnI64(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::U8(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnU8(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::U16(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnU16(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::U32(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnU32(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::U64(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnU64(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::F32(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnF32(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::F64(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnF64(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::Str(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnStr(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::List(_)) => { + Err(ConfigObjectAccessError::AccessWithKeyOnList(k.to_string())) + } + (Some(AccessType::Key(k)), ConfigElement::Map(hm)) => { + if let Some(value) = hm.get(k.as_str()) { + accessor.advance(); + if accessor.current().is_none() { + return Ok(Some(value)); + } else { + value.get(accessor) + } + } else { + Ok(None) + } + } + + (Some(AccessType::Index(u)), ConfigElement::Null) => { + Err(ConfigObjectAccessError::AccessWithIndexOnNull(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::Bool(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnBool(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::I8(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnI8(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::I16(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnI16(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::I32(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnI32(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::I64(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnI64(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::U8(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnU8(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::U16(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnU16(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::U32(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnU32(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::U64(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnU64(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::F32(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnF32(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::F64(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnF64(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::Str(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnStr(*u)) + } + (Some(AccessType::Index(u)), ConfigElement::List(v)) => { + if let Some(value) = v.get(*u) { + accessor.advance(); + if accessor.current().is_none() { + return Ok(Some(value)); + } else { + value.get(accessor) + } + } else { + Ok(None) + } + } + (Some(AccessType::Index(u)), ConfigElement::Map(_)) => { + Err(ConfigObjectAccessError::AccessWithIndexOnMap(*u)) + } + + (None, _) => Err(ConfigObjectAccessError::NoAccessor), + } + } +} + +pub trait IntoConfigElement { + type Error: std::error::Error; + + fn into_config_element(self) -> Result; +} + +#[cfg(feature = "json")] +pub mod json; + +#[cfg(feature = "toml")] +pub mod toml; + +#[cfg(test)] +mod tests { + #[test] + #[cfg(feature = "toml")] + fn test_nested_toml_config() { + use crate::element::ConfigElement; + use crate::element::IntoConfigElement; + use crate::Config; + + let toml: toml::Value = toml::from_str( + r#" + key1 = "value2" + + [table] + key2 = "value3" + "#, + ) + .unwrap(); + + let source = crate::source::test_source::TestSource(toml.into_config_element().unwrap()); + + let c = Config::builder().load(Box::new(source)).build().unwrap(); + + let r = c.get("key1"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value2"), + _ => panic!(), + } + + let r = c.get("table.key2"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "value3"), + _ => panic!(), + } + } + + #[test] + #[cfg(feature = "toml")] + fn test_nested_toml_config_with_index() { + use crate::element::ConfigElement; + use crate::element::IntoConfigElement; + use crate::Config; + + let toml: toml::Value = toml::from_str( + r#" + [[key]] + k = "a" + + [[key]] + k = "b" + "#, + ) + .unwrap(); + + let source = crate::source::test_source::TestSource(toml.into_config_element().unwrap()); + + let c = Config::builder().load(Box::new(source)).build().unwrap(); + + let r = c.get("key.0.k"); + assert!(r.is_ok()); + let r = r.unwrap(); + assert!(r.is_some()); + let r = r.unwrap(); + match r { + ConfigElement::Str(s) => assert_eq!(s, "a"), + _ => panic!(), + } + } +} diff --git a/src/element/toml.rs b/src/element/toml.rs new file mode 100644 index 00000000..663e4e1b --- /dev/null +++ b/src/element/toml.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +use crate::element::ConfigElement; +use crate::element::IntoConfigElement; + +#[derive(Debug, thiserror::Error)] +pub enum TomlIntoConfigElementError {} + +impl IntoConfigElement for toml::Value { + type Error = TomlIntoConfigElementError; + + fn into_config_element(self) -> Result { + match self { + toml::Value::String(s) => Ok(ConfigElement::Str(s)), + toml::Value::Integer(i) => Ok(ConfigElement::I64(i)), + toml::Value::Float(f) => Ok(ConfigElement::F64(f)), + toml::Value::Boolean(b) => Ok(ConfigElement::Bool(b)), + toml::Value::Datetime(_) => unimplemented!(), // TODO + toml::Value::Array(ary) => ary + .into_iter() + .map(|e| e.into_config_element()) + .collect::, Self::Error>>() + .map(ConfigElement::List), + toml::Value::Table(table) => table + .into_iter() + .map(|(k, v)| v.into_config_element().map(|v| (k.to_string(), v))) + .collect::, Self::Error>>() + .map(ConfigElement::Map), + } + } +} + +#[cfg(test)] +mod tests { + use crate::element::ConfigElement; + + #[test] + fn deser_toml_1() { + let s = r#" + key = "value" + "#; + + let e: ConfigElement = toml::from_str(s).unwrap(); + match e { + ConfigElement::Map(map) => { + assert_eq!( + *map.get("key").unwrap(), + ConfigElement::Str("value".to_string()) + ); + } + _ => panic!("Not a map"), + } + } +} diff --git a/src/env.rs b/src/env.rs deleted file mode 100644 index 432df2c8..00000000 --- a/src/env.rs +++ /dev/null @@ -1,244 +0,0 @@ -use std::env; - -use crate::error::Result; -use crate::map::Map; -use crate::source::Source; -use crate::value::{Value, ValueKind}; - -#[must_use] -#[derive(Clone, Debug, Default)] -pub struct Environment { - /// Optional prefix that will limit access to the environment to only keys that - /// begin with the defined prefix. - /// - /// A prefix with a separator of `_` is tested to be present on each key before its considered - /// to be part of the source environment. - /// - /// For example, the key `CONFIG_DEBUG` would become `DEBUG` with a prefix of `config`. - prefix: Option, - - /// Optional character sequence that separates the prefix from the rest of the key - prefix_separator: Option, - - /// Optional character sequence that separates each key segment in an environment key pattern. - /// Consider a nested configuration such as `redis.password`, a separator of `_` would allow - /// an environment key of `REDIS_PASSWORD` to match. - separator: Option, - - /// Optional character sequence that separates each env value into a vector. only works when try_parsing is set to true - /// Once set, you cannot have type String on the same environment, unless you set list_parse_keys. - list_separator: Option, - /// A list of keys which should always be parsed as a list. If not set you can have only Vec or String (not both) in one environment. - list_parse_keys: Option>, - - /// Ignore empty env values (treat as unset). - ignore_empty: bool, - - /// Parses booleans, integers and floats if they're detected (can be safely parsed). - try_parsing: bool, - - // Preserve the prefix while parsing - keep_prefix: bool, - - /// Alternate source for the environment. This can be used when you want to test your own code - /// using this source, without the need to change the actual system environment variables. - /// - /// ## Example - /// - /// ```rust - /// # use config::{Environment, Config}; - /// # use serde::Deserialize; - /// # use std::collections::HashMap; - /// # use std::convert::TryInto; - /// # - /// #[test] - /// fn test_config() -> Result<(), config::ConfigError> { - /// #[derive(Clone, Debug, Deserialize)] - /// struct MyConfig { - /// pub my_string: String, - /// } - /// - /// let source = Environment::default() - /// .source(Some({ - /// let mut env = HashMap::new(); - /// env.insert("MY_STRING".into(), "my-value".into()); - /// env - /// })); - /// - /// let config: MyConfig = Config::builder() - /// .add_source(source) - /// .build()? - /// .try_into()?; - /// assert_eq!(config.my_string, "my-value"); - /// - /// Ok(()) - /// } - /// ``` - source: Option>, -} - -impl Environment { - #[deprecated(since = "0.12.0", note = "please use 'Environment::default' instead")] - pub fn new() -> Self { - Self::default() - } - - pub fn with_prefix(s: &str) -> Self { - Self { - prefix: Some(s.into()), - ..Self::default() - } - } - - pub fn prefix(mut self, s: &str) -> Self { - self.prefix = Some(s.into()); - self - } - - pub fn prefix_separator(mut self, s: &str) -> Self { - self.prefix_separator = Some(s.into()); - self - } - - pub fn separator(mut self, s: &str) -> Self { - self.separator = Some(s.into()); - self - } - - /// When set and try_parsing is true, then all environment variables will be parsed as [`Vec`] instead of [`String`]. - /// See [`with_list_parse_key`] when you want to use [`Vec`] in combination with [`String`]. - pub fn list_separator(mut self, s: &str) -> Self { - self.list_separator = Some(s.into()); - self - } - - /// Add a key which should be parsed as a list when collecting [`Value`]s from the environment. - /// Once list_separator is set, the type for string is [`Vec`]. - /// To switch the default type back to type Strings you need to provide the keys which should be [`Vec`] using this function. - pub fn with_list_parse_key(mut self, key: &str) -> Self { - if self.list_parse_keys == None { - self.list_parse_keys = Some(vec![key.into()]) - } else { - self.list_parse_keys = self.list_parse_keys.map(|mut keys| { - keys.push(key.into()); - keys - }); - } - self - } - - pub fn ignore_empty(mut self, ignore: bool) -> Self { - self.ignore_empty = ignore; - self - } - - /// Note: enabling `try_parsing` can reduce performance it will try and parse - /// each environment variable 3 times (bool, i64, f64) - pub fn try_parsing(mut self, try_parsing: bool) -> Self { - self.try_parsing = try_parsing; - self - } - - pub fn keep_prefix(mut self, keep: bool) -> Self { - self.keep_prefix = keep; - self - } - - pub fn source(mut self, source: Option>) -> Self { - self.source = source; - self - } -} - -impl Source for Environment { - fn clone_into_box(&self) -> Box { - Box::new((*self).clone()) - } - - fn collect(&self) -> Result> { - let mut m = Map::new(); - let uri: String = "the environment".into(); - - let separator = self.separator.as_deref().unwrap_or(""); - let prefix_separator = match (self.prefix_separator.as_deref(), self.separator.as_deref()) { - (Some(pre), _) => pre, - (None, Some(sep)) => sep, - (None, None) => "_", - }; - - // Define a prefix pattern to test and exclude from keys - let prefix_pattern = self - .prefix - .as_ref() - .map(|prefix| format!("{}{}", prefix, prefix_separator).to_lowercase()); - - let collector = |(key, value): (String, String)| { - // Treat empty environment variables as unset - if self.ignore_empty && value.is_empty() { - return; - } - - let mut key = key.to_lowercase(); - - // Check for prefix - if let Some(ref prefix_pattern) = prefix_pattern { - if key.starts_with(prefix_pattern) { - if !self.keep_prefix { - // Remove this prefix from the key - key = key[prefix_pattern.len()..].to_string(); - } - } else { - // Skip this key - return; - } - } - - // If separator is given replace with `.` - if !separator.is_empty() { - key = key.replace(separator, "."); - } - - let value = if self.try_parsing { - // convert to lowercase because bool parsing expects all lowercase - if let Ok(parsed) = value.to_lowercase().parse::() { - ValueKind::Boolean(parsed) - } else if let Ok(parsed) = value.parse::() { - ValueKind::I64(parsed) - } else if let Ok(parsed) = value.parse::() { - ValueKind::Float(parsed) - } else if let Some(separator) = &self.list_separator { - if let Some(keys) = &self.list_parse_keys { - if keys.contains(&key) { - let v: Vec = value - .split(separator) - .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string()))) - .collect(); - ValueKind::Array(v) - } else { - ValueKind::String(value) - } - } else { - let v: Vec = value - .split(separator) - .map(|s| Value::new(Some(&uri), ValueKind::String(s.to_string()))) - .collect(); - ValueKind::Array(v) - } - } else { - ValueKind::String(value) - } - } else { - ValueKind::String(value) - }; - - m.insert(key, Value::new(Some(&uri), value)); - }; - - match &self.source { - Some(source) => source.clone().into_iter().for_each(collector), - None => env::vars().for_each(collector), - } - - Ok(m) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 7957d2f1..00000000 --- a/src/error.rs +++ /dev/null @@ -1,244 +0,0 @@ -use std::error::Error; -use std::fmt; -use std::result; - -use serde::de; -use serde::ser; - -#[derive(Debug)] -pub enum Unexpected { - Bool(bool), - I64(i64), - I128(i128), - U64(u64), - U128(u128), - Float(f64), - Str(String), - Unit, - Seq, - Map, -} - -impl fmt::Display for Unexpected { - fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { - match *self { - Unexpected::Bool(b) => write!(f, "boolean `{}`", b), - Unexpected::I64(i) => write!(f, "64-bit integer `{}`", i), - Unexpected::I128(i) => write!(f, "128-bit integer `{}`", i), - Unexpected::U64(i) => write!(f, "64-bit unsigned integer `{}`", i), - Unexpected::U128(i) => write!(f, "128-bit unsigned integer `{}`", i), - Unexpected::Float(v) => write!(f, "floating point `{}`", v), - Unexpected::Str(ref s) => write!(f, "string {:?}", s), - Unexpected::Unit => write!(f, "unit value"), - Unexpected::Seq => write!(f, "sequence"), - Unexpected::Map => write!(f, "map"), - } - } -} - -/// Represents all possible errors that can occur when working with -/// configuration. -pub enum ConfigError { - /// Configuration is frozen and no further mutations can be made. - Frozen, - - /// Configuration property was not found - NotFound(String), - - /// Configuration path could not be parsed. - PathParse(nom::error::ErrorKind), - - /// Configuration could not be parsed from file. - FileParse { - /// The URI used to access the file (if not loaded from a string). - /// Example: `/path/to/config.json` - uri: Option, - - /// The captured error from attempting to parse the file in its desired format. - /// This is the actual error object from the library used for the parsing. - cause: Box, - }, - - /// Value could not be converted into the requested type. - Type { - /// The URI that references the source that the value came from. - /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost` - // TODO: Why is this called Origin but FileParse has a uri field? - origin: Option, - - /// What we found when parsing the value - unexpected: Unexpected, - - /// What was expected when parsing the value - expected: &'static str, - - /// The key in the configuration hash of this value (if available where the - /// error is generated). - key: Option, - }, - - /// Custom message - Message(String), - - /// Unadorned error from a foreign origin. - Foreign(Box), -} - -impl ConfigError { - // FIXME: pub(crate) - #[doc(hidden)] - pub fn invalid_type( - origin: Option, - unexpected: Unexpected, - expected: &'static str, - ) -> Self { - Self::Type { - origin, - unexpected, - expected, - key: None, - } - } - - // Have a proper error fire if the root of a file is ever not a Table - // TODO: for now only json5 checked, need to finish others - #[doc(hidden)] - pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box { - Box::new(Self::Type { - origin: origin.cloned(), - unexpected, - expected: "a map", - key: None, - }) - } - - // FIXME: pub(crate) - #[doc(hidden)] - #[must_use] - pub fn extend_with_key(self, key: &str) -> Self { - match self { - Self::Type { - origin, - unexpected, - expected, - .. - } => Self::Type { - origin, - unexpected, - expected, - key: Some(key.into()), - }, - - _ => self, - } - } - - #[must_use] - fn prepend(self, segment: &str, add_dot: bool) -> Self { - let concat = |key: Option| { - let key = key.unwrap_or_default(); - let dot = if add_dot && key.as_bytes().get(0).unwrap_or(&b'[') != &b'[' { - "." - } else { - "" - }; - format!("{}{}{}", segment, dot, key) - }; - match self { - Self::Type { - origin, - unexpected, - expected, - key, - } => Self::Type { - origin, - unexpected, - expected, - key: Some(concat(key)), - }, - Self::NotFound(key) => Self::NotFound(concat(Some(key))), - _ => self, - } - } - - #[must_use] - pub(crate) fn prepend_key(self, key: &str) -> Self { - self.prepend(key, true) - } - - #[must_use] - pub(crate) fn prepend_index(self, idx: usize) -> Self { - self.prepend(&format!("[{}]", idx), false) - } -} - -/// Alias for a `Result` with the error type set to `ConfigError`. -pub type Result = result::Result; - -// Forward Debug to Display for readable panic! messages -impl fmt::Debug for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", *self) - } -} - -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ConfigError::Frozen => write!(f, "configuration is frozen"), - - ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()), - - ConfigError::Message(ref s) => write!(f, "{}", s), - - ConfigError::Foreign(ref cause) => write!(f, "{}", cause), - - ConfigError::NotFound(ref key) => { - write!(f, "configuration property {:?} not found", key) - } - - ConfigError::Type { - ref origin, - ref unexpected, - expected, - ref key, - } => { - write!(f, "invalid type: {}, expected {}", unexpected, expected)?; - - if let Some(ref key) = *key { - write!(f, " for key `{}`", key)?; - } - - if let Some(ref origin) = *origin { - write!(f, " in {}", origin)?; - } - - Ok(()) - } - - ConfigError::FileParse { ref cause, ref uri } => { - write!(f, "{}", cause)?; - - if let Some(ref uri) = *uri { - write!(f, " in {}", uri)?; - } - - Ok(()) - } - } - } -} - -impl Error for ConfigError {} - -impl de::Error for ConfigError { - fn custom(msg: T) -> Self { - Self::Message(msg.to_string()) - } -} - -impl ser::Error for ConfigError { - fn custom(msg: T) -> Self { - Self::Message(msg.to_string()) - } -} diff --git a/src/file/format/ini.rs b/src/file/format/ini.rs deleted file mode 100644 index 9295e60e..00000000 --- a/src/file/format/ini.rs +++ /dev/null @@ -1,37 +0,0 @@ -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, Box> { - let mut map: Map = Map::new(); - let i = Ini::load_from_str(text)?; - for (sec, prop) in i.iter() { - match sec { - Some(sec) => { - let mut sec_map: Map = Map::new(); - for (k, v) in prop.iter() { - sec_map.insert( - k.to_owned(), - Value::new(uri, ValueKind::String(v.to_owned())), - ); - } - map.insert(sec.to_owned(), Value::new(uri, ValueKind::Table(sec_map))); - } - None => { - for (k, v) in prop.iter() { - map.insert( - k.to_owned(), - Value::new(uri, ValueKind::String(v.to_owned())), - ); - } - } - } - } - Ok(map) -} diff --git a/src/file/format/json.rs b/src/file/format/json.rs deleted file mode 100644 index 1720cb60..00000000 --- a/src/file/format/json.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::error::Error; - -use crate::map::Map; -use crate::value::{Value, ValueKind}; - -pub fn parse( - uri: Option<&String>, - text: &str, -) -> Result, Box> { - // 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(Map::new()), - } -} - -fn from_json_value(uri: Option<&String>, value: &serde_json::Value) -> Value { - match *value { - serde_json::Value::String(ref value) => Value::new(uri, ValueKind::String(value.clone())), - - serde_json::Value::Number(ref value) => { - if let Some(value) = value.as_i64() { - Value::new(uri, ValueKind::I64(value)) - } else if let Some(value) = value.as_f64() { - Value::new(uri, ValueKind::Float(value)) - } else { - unreachable!(); - } - } - - serde_json::Value::Bool(value) => Value::new(uri, ValueKind::Boolean(value)), - - serde_json::Value::Object(ref table) => { - let mut m = Map::new(); - - for (key, value) in table { - m.insert(key.clone(), from_json_value(uri, value)); - } - - Value::new(uri, ValueKind::Table(m)) - } - - serde_json::Value::Array(ref array) => { - let mut l = Vec::new(); - - for value in array { - l.push(from_json_value(uri, value)); - } - - Value::new(uri, ValueKind::Array(l)) - } - - serde_json::Value::Null => Value::new(uri, ValueKind::Nil), - } -} diff --git a/src/file/format/json5.rs b/src/file/format/json5.rs deleted file mode 100644 index 92aaa8f9..00000000 --- a/src/file/format/json5.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::error::Error; - -use crate::error::{ConfigError, Unexpected}; -use crate::map::Map; -use crate::value::{Value, ValueKind}; - -#[derive(serde::Deserialize, Debug)] -#[serde(untagged)] -pub enum Val { - Null, - Boolean(bool), - Integer(i64), - Float(f64), - String(String), - Array(Vec), - Object(Map), -} - -pub fn parse( - uri: Option<&String>, - text: &str, -) -> Result, Box> { - match json5_rs::from_str::(text)? { - Val::String(ref value) => Err(Unexpected::Str(value.clone())), - Val::Integer(value) => Err(Unexpected::I64(value)), - Val::Float(value) => Err(Unexpected::Float(value)), - Val::Boolean(value) => Err(Unexpected::Bool(value)), - Val::Array(_) => Err(Unexpected::Seq), - Val::Null => Err(Unexpected::Unit), - Val::Object(o) => match from_json5_value(uri, Val::Object(o)).kind { - ValueKind::Table(map) => Ok(map), - _ => Ok(Map::new()), - }, - } - .map_err(|err| ConfigError::invalid_root(uri, err)) - .map_err(|err| Box::new(err) as Box) -} - -fn from_json5_value(uri: Option<&String>, value: Val) -> Value { - let vk = match value { - Val::Null => ValueKind::Nil, - Val::String(v) => ValueKind::String(v), - Val::Integer(v) => ValueKind::I64(v), - Val::Float(v) => ValueKind::Float(v), - Val::Boolean(v) => ValueKind::Boolean(v), - Val::Object(table) => { - let m = table - .into_iter() - .map(|(k, v)| (k, from_json5_value(uri, v))) - .collect(); - - ValueKind::Table(m) - } - - Val::Array(array) => { - let l = array - .into_iter() - .map(|v| from_json5_value(uri, v)) - .collect(); - - ValueKind::Array(l) - } - }; - - Value::new(uri, vk) -} diff --git a/src/file/format/mod.rs b/src/file/format/mod.rs deleted file mode 100644 index 025e98a9..00000000 --- a/src/file/format/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -// If no features are used, there is an "unused mut" warning in `ALL_EXTENSIONS` -// BUG: ? For some reason this doesn't do anything if I try and function scope this -#![allow(unused_mut)] - -use lazy_static::lazy_static; -use std::collections::HashMap; -use std::error::Error; - -use crate::map::Map; -use crate::{file::FileStoredFormat, value::Value, Format}; - -#[cfg(feature = "toml")] -mod toml; - -#[cfg(feature = "json")] -mod json; - -#[cfg(feature = "yaml")] -mod yaml; - -#[cfg(feature = "ini")] -mod ini; - -#[cfg(feature = "ron")] -mod ron; - -#[cfg(feature = "json5")] -mod json5; - -/// File formats provided by the library. -/// -/// Although it is possible to define custom formats using [`Format`] trait it is recommended to use FileFormat if possible. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub enum FileFormat { - /// TOML (parsed with toml) - #[cfg(feature = "toml")] - Toml, - - /// JSON (parsed with serde_json) - #[cfg(feature = "json")] - Json, - - /// YAML (parsed with yaml_rust) - #[cfg(feature = "yaml")] - Yaml, - - /// INI (parsed with rust_ini) - #[cfg(feature = "ini")] - Ini, - - /// RON (parsed with ron) - #[cfg(feature = "ron")] - Ron, - - /// JSON5 (parsed with json5) - #[cfg(feature = "json5")] - Json5, -} - -lazy_static! { - #[doc(hidden)] - // #[allow(unused_mut)] ? - pub static ref ALL_EXTENSIONS: HashMap> = { - let mut formats: HashMap> = HashMap::new(); - - #[cfg(feature = "toml")] - formats.insert(FileFormat::Toml, vec!["toml"]); - - #[cfg(feature = "json")] - formats.insert(FileFormat::Json, vec!["json"]); - - #[cfg(feature = "yaml")] - formats.insert(FileFormat::Yaml, vec!["yaml", "yml"]); - - #[cfg(feature = "ini")] - formats.insert(FileFormat::Ini, vec!["ini"]); - - #[cfg(feature = "ron")] - formats.insert(FileFormat::Ron, vec!["ron"]); - - #[cfg(feature = "json5")] - formats.insert(FileFormat::Json5, vec!["json5"]); - - formats - }; -} - -impl FileFormat { - pub(crate) fn extensions(&self) -> &'static [&'static str] { - // It should not be possible for this to fail - // A FileFormat would need to be declared without being added to the - // ALL_EXTENSIONS map. - ALL_EXTENSIONS.get(self).unwrap() - } - - pub(crate) fn parse( - &self, - uri: Option<&String>, - text: &str, - ) -> Result, Box> { - match self { - #[cfg(feature = "toml")] - FileFormat::Toml => toml::parse(uri, text), - - #[cfg(feature = "json")] - FileFormat::Json => json::parse(uri, text), - - #[cfg(feature = "yaml")] - FileFormat::Yaml => yaml::parse(uri, text), - - #[cfg(feature = "ini")] - FileFormat::Ini => ini::parse(uri, text), - - #[cfg(feature = "ron")] - FileFormat::Ron => ron::parse(uri, text), - - #[cfg(feature = "json5")] - FileFormat::Json5 => json5::parse(uri, text), - - #[cfg(all( - not(feature = "toml"), - not(feature = "json"), - not(feature = "yaml"), - not(feature = "ini"), - not(feature = "ron"), - not(feature = "json5"), - ))] - _ => unreachable!("No features are enabled, this library won't work without features"), - } - } -} - -impl Format for FileFormat { - fn parse( - &self, - uri: Option<&String>, - text: &str, - ) -> Result, Box> { - self.parse(uri, text) - } -} - -impl FileStoredFormat for FileFormat { - fn file_extensions(&self) -> &'static [&'static str] { - self.extensions() - } -} diff --git a/src/file/format/ron.rs b/src/file/format/ron.rs deleted file mode 100644 index fb2b0636..00000000 --- a/src/file/format/ron.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::error::Error; - -use crate::map::Map; -use crate::value::{Value, ValueKind}; - -pub fn parse( - uri: Option<&String>, - text: &str, -) -> Result, Box> { - let value = from_ron_value(uri, ron::from_str(text)?)?; - match value.kind { - ValueKind::Table(map) => Ok(map), - - _ => Ok(Map::new()), - } -} - -fn from_ron_value( - uri: Option<&String>, - value: ron::Value, -) -> Result> { - let kind = match value { - ron::Value::Option(value) => match value { - Some(value) => from_ron_value(uri, *value)?.kind, - None => ValueKind::Nil, - }, - - ron::Value::Unit => ValueKind::Nil, - - ron::Value::Bool(value) => ValueKind::Boolean(value), - - ron::Value::Number(value) => match value { - ron::Number::Float(value) => ValueKind::Float(value.get()), - ron::Number::Integer(value) => ValueKind::I64(value), - }, - - ron::Value::Char(value) => ValueKind::String(value.to_string()), - - ron::Value::String(value) => ValueKind::String(value), - - ron::Value::Seq(values) => { - let array = values - .into_iter() - .map(|value| from_ron_value(uri, value)) - .collect::, _>>()?; - - ValueKind::Array(array) - } - - ron::Value::Map(values) => { - let map = values - .iter() - .map(|(key, value)| -> Result<_, Box> { - let key = key.clone().into_rust::()?; - let value = from_ron_value(uri, value.clone())?; - - Ok((key, value)) - }) - .collect::, _>>()?; - - ValueKind::Table(map) - } - }; - - Ok(Value::new(uri, kind)) -} diff --git a/src/file/format/toml.rs b/src/file/format/toml.rs deleted file mode 100644 index af21fc78..00000000 --- a/src/file/format/toml.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::error::Error; - -use crate::map::Map; -use crate::value::{Value, ValueKind}; - -pub fn parse( - uri: Option<&String>, - text: &str, -) -> Result, Box> { - // 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(Map::new()), - } -} - -fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value { - match *value { - toml::Value::String(ref value) => Value::new(uri, value.to_string()), - toml::Value::Float(value) => Value::new(uri, value), - toml::Value::Integer(value) => Value::new(uri, value), - toml::Value::Boolean(value) => Value::new(uri, value), - - toml::Value::Table(ref table) => { - let mut m = Map::new(); - - for (key, value) in table { - m.insert(key.clone(), from_toml_value(uri, value)); - } - - Value::new(uri, m) - } - - toml::Value::Array(ref array) => { - let mut l = Vec::new(); - - for value in array { - l.push(from_toml_value(uri, value)); - } - - Value::new(uri, l) - } - - toml::Value::Datetime(ref datetime) => Value::new(uri, datetime.to_string()), - } -} diff --git a/src/file/format/yaml.rs b/src/file/format/yaml.rs deleted file mode 100644 index 2a76261c..00000000 --- a/src/file/format/yaml.rs +++ /dev/null @@ -1,108 +0,0 @@ -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, Box> { - // Parse a YAML object from file - let mut docs = yaml::YamlLoader::load_from_str(text)?; - let root = match docs.len() { - 0 => yaml::Yaml::Hash(yaml::yaml::Hash::new()), - 1 => mem::replace(&mut docs[0], yaml::Yaml::Null), - n => { - return Err(Box::new(MultipleDocumentsError(n))); - } - }; - - // TODO: Have a proper error fire if the root of a file is ever not a Table - let value = from_yaml_value(uri, &root)?; - match value.kind { - ValueKind::Table(map) => Ok(map), - - _ => Ok(Map::new()), - } -} - -fn from_yaml_value( - uri: Option<&String>, - value: &yaml::Yaml, -) -> Result> { - match *value { - yaml::Yaml::String(ref value) => Ok(Value::new(uri, ValueKind::String(value.clone()))), - yaml::Yaml::Real(ref value) => { - // TODO: Figure out in what cases this can panic? - value - .parse::() - .map_err(|_| { - Box::new(FloatParsingError(value.to_string())) as Box<(dyn Error + Send + Sync)> - }) - .map(ValueKind::Float) - .map(|f| Value::new(uri, f)) - } - yaml::Yaml::Integer(value) => Ok(Value::new(uri, ValueKind::I64(value))), - yaml::Yaml::Boolean(value) => Ok(Value::new(uri, ValueKind::Boolean(value))), - yaml::Yaml::Hash(ref table) => { - 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)?); - } - // TODO: should we do anything for non-string keys? - } - Ok(Value::new(uri, ValueKind::Table(m))) - } - yaml::Yaml::Array(ref array) => { - let mut l = Vec::new(); - - for value in array { - l.push(from_yaml_value(uri, value)?); - } - - Ok(Value::new(uri, ValueKind::Array(l))) - } - - // 1. Yaml NULL - // 2. BadValue – It shouldn't be possible to hit BadValue as this only happens when - // using the index trait badly or on a type error but we send back nil. - // 3. Alias – No idea what to do with this and there is a note in the lib that its - // not fully supported yet anyway - _ => Ok(Value::new(uri, ValueKind::Nil)), - } -} - -#[derive(Debug, Copy, Clone)] -struct MultipleDocumentsError(usize); - -impl fmt::Display for MultipleDocumentsError { - fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result { - write!(format, "Got {} YAML documents, expected 1", self.0) - } -} - -impl Error for MultipleDocumentsError { - fn description(&self) -> &str { - "More than one YAML document provided" - } -} - -#[derive(Debug, Clone)] -struct FloatParsingError(String); - -impl fmt::Display for FloatParsingError { - fn fmt(&self, format: &mut fmt::Formatter) -> fmt::Result { - write!(format, "Parsing {} as floating point number failed", self.0) - } -} - -impl Error for FloatParsingError { - fn description(&self) -> &str { - "Floating point number parsing failed" - } -} diff --git a/src/file/mod.rs b/src/file/mod.rs deleted file mode 100644 index 65f3fd65..00000000 --- a/src/file/mod.rs +++ /dev/null @@ -1,149 +0,0 @@ -mod format; -pub mod source; - -use std::fmt::Debug; -use std::path::{Path, PathBuf}; - -use crate::error::{ConfigError, Result}; -use crate::map::Map; -use crate::source::Source; -use crate::value::Value; -use crate::Format; - -pub use self::format::FileFormat; -use self::source::FileSource; - -pub use self::source::file::FileSourceFile; -pub use self::source::string::FileSourceString; - -/// A configuration source backed up by a file. -/// -/// It supports optional automatic file format discovery. -#[derive(Clone, Debug)] -pub struct File { - source: T, - - /// Format of file (which dictates what driver to use). - format: Option, - - /// A required File will error if it cannot be found - required: bool, -} - -/// An extension of [`Format`](crate::Format) trait. -/// -/// Associates format with file extensions, therefore linking storage-agnostic notion of format to a file system. -pub trait FileStoredFormat: Format { - /// Returns a vector of file extensions, for instance `[yml, yaml]`. - fn file_extensions(&self) -> &'static [&'static str]; -} - -impl File -where - F: FileStoredFormat + 'static, -{ - pub fn from_str(s: &str, format: F) -> Self { - Self { - format: Some(format), - required: true, - source: s.into(), - } - } -} - -impl File -where - F: FileStoredFormat + 'static, -{ - pub fn new(name: &str, format: F) -> Self { - Self { - format: Some(format), - required: true, - source: source::file::FileSourceFile::new(name.into()), - } - } -} - -impl File { - /// Given the basename of a file, will attempt to locate a file by setting its - /// extension to a registered format. - pub fn with_name(name: &str) -> Self { - Self { - format: None, - required: true, - source: source::file::FileSourceFile::new(name.into()), - } - } -} - -impl<'a> From<&'a Path> for File { - fn from(path: &'a Path) -> Self { - Self { - format: None, - required: true, - source: source::file::FileSourceFile::new(path.to_path_buf()), - } - } -} - -impl From for File { - fn from(path: PathBuf) -> Self { - Self { - format: None, - required: true, - source: source::file::FileSourceFile::new(path), - } - } -} - -impl File -where - F: FileStoredFormat + 'static, - T: FileSource, -{ - #[must_use] - pub fn format(mut self, format: F) -> Self { - self.format = Some(format); - self - } - - #[must_use] - pub fn required(mut self, required: bool) -> Self { - self.required = required; - self - } -} - -impl Source for File -where - F: FileStoredFormat + Debug + Clone + Send + Sync + 'static, - T: Sync + Send + FileSource + 'static, -{ - fn clone_into_box(&self) -> Box { - Box::new((*self).clone()) - } - - fn collect(&self) -> Result> { - // Coerce the file contents to a string - let (uri, contents, format) = match self - .source - .resolve(self.format.clone()) - .map_err(|err| ConfigError::Foreign(err)) - { - Ok(result) => (result.uri, result.content, result.format), - - Err(error) => { - if !self.required { - return Ok(Map::new()); - } - - return Err(error); - } - }; - - // Parse the string using the given format - format - .parse(uri.as_ref(), &contents) - .map_err(|cause| ConfigError::FileParse { uri, cause }) - } -} diff --git a/src/file/source/file.rs b/src/file/source/file.rs deleted file mode 100644 index 8ee5d315..00000000 --- a/src/file/source/file.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::env; -use std::error::Error; -use std::fs; -use std::io; -use std::path::PathBuf; - -use crate::file::{ - format::ALL_EXTENSIONS, source::FileSourceResult, FileSource, FileStoredFormat, Format, -}; - -/// Describes a file sourced from a file -#[derive(Clone, Debug)] -pub struct FileSourceFile { - /// Path of configuration file - name: PathBuf, -} - -impl FileSourceFile { - pub fn new(name: PathBuf) -> Self { - Self { name } - } - - fn find_file( - &self, - format_hint: Option, - ) -> Result<(PathBuf, Box), Box> - where - F: FileStoredFormat + Format + 'static, - { - let filename = if self.name.is_absolute() { - self.name.clone() - } else { - env::current_dir()?.as_path().join(&self.name) - }; - - // First check for an _exact_ match - if filename.is_file() { - return if let Some(format) = format_hint { - Ok((filename, Box::new(format))) - } else { - for (format, extensions) in ALL_EXTENSIONS.iter() { - if extensions.contains( - &filename - .extension() - .unwrap_or_default() - .to_string_lossy() - .as_ref(), - ) { - return Ok((filename, Box::new(*format))); - } - } - - Err(Box::new(io::Error::new( - io::ErrorKind::NotFound, - format!( - "configuration file \"{}\" is not of a registered file format", - filename.to_string_lossy() - ), - ))) - }; - } - // Adding a dummy extension will make sure we will not override secondary extensions, i.e. "file.local" - // This will make the following set_extension function calls to append the extension. - let mut filename = add_dummy_extension(filename); - - match format_hint { - Some(format) => { - for ext in format.file_extensions() { - filename.set_extension(ext); - - if filename.is_file() { - return Ok((filename, Box::new(format))); - } - } - } - - None => { - for format in ALL_EXTENSIONS.keys() { - for ext in format.extensions() { - filename.set_extension(ext); - - if filename.is_file() { - return Ok((filename, Box::new(*format))); - } - } - } - } - } - - Err(Box::new(io::Error::new( - io::ErrorKind::NotFound, - format!( - "configuration file \"{}\" not found", - self.name.to_string_lossy() - ), - ))) - } -} - -impl FileSource for FileSourceFile -where - F: Format + FileStoredFormat + 'static, -{ - fn resolve( - &self, - format_hint: Option, - ) -> Result> { - // Find file - let (filename, format) = self.find_file(format_hint)?; - - // Attempt to use a relative path for the URI - let uri = env::current_dir() - .ok() - .and_then(|base| pathdiff::diff_paths(&filename, base)) - .unwrap_or_else(|| filename.clone()); - - // Read contents from file - let text = fs::read_to_string(filename)?; - - Ok(FileSourceResult { - uri: Some(uri.to_string_lossy().into_owned()), - content: text, - format, - }) - } -} - -fn add_dummy_extension(mut filename: PathBuf) -> PathBuf { - match filename.extension() { - Some(extension) => { - let mut ext = extension.to_os_string(); - ext.push("."); - ext.push("dummy"); - filename.set_extension(ext); - } - None => { - filename.set_extension("dummy"); - } - } - filename -} diff --git a/src/file/source/mod.rs b/src/file/source/mod.rs deleted file mode 100644 index 3c3d10ca..00000000 --- a/src/file/source/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod file; -pub mod string; - -use std::error::Error; -use std::fmt::Debug; - -use crate::{file::FileStoredFormat, Format}; - -/// Describes where the file is sourced -pub trait FileSource: Debug + Clone -where - T: Format + FileStoredFormat, -{ - fn resolve( - &self, - format_hint: Option, - ) -> Result>; -} - -pub struct FileSourceResult { - pub(crate) uri: Option, - pub(crate) content: String, - pub(crate) format: Box, -} - -impl FileSourceResult { - pub fn uri(&self) -> &Option { - &self.uri - } - - pub fn content(&self) -> &str { - self.content.as_str() - } - - pub fn format(&self) -> &dyn Format { - self.format.as_ref() - } -} diff --git a/src/file/source/string.rs b/src/file/source/string.rs deleted file mode 100644 index 89300d4a..00000000 --- a/src/file/source/string.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::error::Error; - -use crate::{ - file::source::FileSourceResult, - file::{FileSource, FileStoredFormat}, - Format, -}; - -/// Describes a file sourced from a string -#[derive(Clone, Debug)] -pub struct FileSourceString(String); - -impl<'a> From<&'a str> for FileSourceString { - fn from(s: &'a str) -> Self { - Self(s.into()) - } -} - -impl FileSource for FileSourceString -where - F: Format + FileStoredFormat + 'static, -{ - fn resolve( - &self, - format_hint: Option, - ) -> Result> { - Ok(FileSourceResult { - uri: None, - content: self.0.clone(), - format: Box::new(format_hint.expect("from_str requires a set file format")), - }) - } -} diff --git a/src/format.rs b/src/format.rs deleted file mode 100644 index ab9e4fa2..00000000 --- a/src/format.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::error::Error; - -use crate::{map::Map, value::Value}; - -/// Describes a format of configuration source data -/// -/// Implementations of this trait can be used to convert [`File`](crate::File) sources to configuration data. -/// -/// There can be various formats, some of them provided by this library, such as JSON, Yaml and other. -/// This trait enables users of the library to easily define their own, even proprietary formats without -/// the need to alter library sources. -/// -/// What is more, it is recommended to use this trait with custom [`Source`](crate::Source)s and their async counterparts. -pub trait Format { - /// Parses provided content into configuration values understood by the library. - /// - /// It also allows specifying optional URI of the source associated with format instance that can facilitate debugging. - fn parse( - &self, - uri: Option<&String>, - text: &str, - ) -> Result, Box>; -} diff --git a/src/lib.rs b/src/lib.rs index 589d2d51..ae51325f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,44 +1,17 @@ -//! Config organizes hierarchical or layered configurations for Rust applications. -//! -//! Config lets you set a set of default parameters and then extend them via merging in -//! configuration from a variety of sources: -//! -//! - Environment variables -//! - String literals in well-known formats -//! - Another Config instance -//! - Files: TOML, JSON, YAML, INI, RON, JSON5 and custom ones defined with Format trait -//! - Manual, programmatic override (via a `.set` method on the Config instance) -//! -//! Additionally, Config supports: -//! -//! - Live watching and re-reading of configuration files -//! - Deep access into the merged configuration via a path syntax -//! - Deserialization via `serde` of the configuration or any subset defined via a path -//! -//! See the [examples](https://github.com/mehcode/config-rs/tree/master/examples) for -//! general usage information. -#![allow(unknown_lints)] -// #![warn(missing_docs)] - -pub mod builder; +mod accessor; mod config; -mod de; -mod env; -mod error; -mod file; -mod format; -mod map; -mod path; -mod ser; +mod description; +mod element; +mod object; mod source; -mod value; -pub use crate::builder::{AsyncConfigBuilder, ConfigBuilder}; +pub use crate::accessor::Accessor; +pub use crate::accessor::AccessType; +pub use crate::accessor::ParsableAccessor; pub use crate::config::Config; -pub use crate::env::Environment; -pub use crate::error::ConfigError; -pub use crate::file::{File, FileFormat, FileSourceFile, FileSourceString, FileStoredFormat}; -pub use crate::format::Format; -pub use crate::map::Map; -pub use crate::source::{AsyncSource, Source}; -pub use crate::value::{Value, ValueKind}; +pub use crate::config::ConfigBuilder; +pub use crate::description::ConfigSourceDescription; +pub use crate::element::ConfigElement; +pub use crate::object::ConfigObject; +pub use crate::source::ConfigSource; +pub use crate::source::StringSource; diff --git a/src/map.rs b/src/map.rs deleted file mode 100644 index 5873f0d2..00000000 --- a/src/map.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(not(feature = "preserve_order"))] -pub type Map = std::collections::HashMap; -#[cfg(feature = "preserve_order")] -pub type Map = indexmap::IndexMap; diff --git a/src/object/mod.rs b/src/object/mod.rs new file mode 100644 index 00000000..ef597793 --- /dev/null +++ b/src/object/mod.rs @@ -0,0 +1,86 @@ +use crate::accessor::Accessor; +use crate::description::ConfigSourceDescription; +use crate::element::ConfigElement; + +#[derive(Clone, Debug)] +pub struct ConfigObject { + element: ConfigElement, + source: ConfigSourceDescription, +} + +impl ConfigObject { + pub(crate) fn new(element: ConfigElement, source: ConfigSourceDescription) -> Self { + Self { element, source } + } + + pub(crate) fn get( + &self, + accessor: &mut Accessor, + ) -> Result, ConfigObjectAccessError> { + self.element.get(accessor) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ConfigObjectAccessError { + #[error("Tried to access with no accessor")] + NoAccessor, + + #[error("Accessed Null with key '{0}'")] + AccessWithKeyOnNull(String), + #[error("Accessed Bool with key '{0}'")] + AccessWithKeyOnBool(String), + #[error("Accessed i8 with key '{0}'")] + AccessWithKeyOnI8(String), + #[error("Accessed i16 with key '{0}'")] + AccessWithKeyOnI16(String), + #[error("Accessed i32 with key '{0}'")] + AccessWithKeyOnI32(String), + #[error("Accessed i64 with key '{0}'")] + AccessWithKeyOnI64(String), + #[error("Accessed u8 with key '{0}'")] + AccessWithKeyOnU8(String), + #[error("Accessed u16 with key '{0}'")] + AccessWithKeyOnU16(String), + #[error("Accessed u32 with key '{0}'")] + AccessWithKeyOnU32(String), + #[error("Accessed u64 with key '{0}'")] + AccessWithKeyOnU64(String), + #[error("Accessed f32 with key '{0}'")] + AccessWithKeyOnF32(String), + #[error("Accessed f64 with key '{0}'")] + AccessWithKeyOnF64(String), + #[error("Accessed String with key '{0}'")] + AccessWithKeyOnStr(String), + #[error("Accessed List with key '{0}'")] + AccessWithKeyOnList(String), + + #[error("Accessed Null with index '{0}'")] + AccessWithIndexOnNull(usize), + #[error("Accessed Bool with index '{0}'")] + AccessWithIndexOnBool(usize), + #[error("Accessed i8 with index '{0}'")] + AccessWithIndexOnI8(usize), + #[error("Accessed i16 with index '{0}'")] + AccessWithIndexOnI16(usize), + #[error("Accessed i32 with index '{0}'")] + AccessWithIndexOnI32(usize), + #[error("Accessed i64 with index '{0}'")] + AccessWithIndexOnI64(usize), + #[error("Accessed u8 with index '{0}'")] + AccessWithIndexOnU8(usize), + #[error("Accessed u16 with index '{0}'")] + AccessWithIndexOnU16(usize), + #[error("Accessed u32 with index '{0}'")] + AccessWithIndexOnU32(usize), + #[error("Accessed u64 with index '{0}'")] + AccessWithIndexOnU64(usize), + #[error("Accessed f32 with index '{0}'")] + AccessWithIndexOnF32(usize), + #[error("Accessed f64 with index '{0}'")] + AccessWithIndexOnF64(usize), + #[error("Accessed usize with index '{0}'")] + AccessWithIndexOnStr(usize), + #[error("Accessed Map with index '{0}'")] + AccessWithIndexOnMap(usize), +} diff --git a/src/path/mod.rs b/src/path/mod.rs deleted file mode 100644 index 7456aa34..00000000 --- a/src/path/mod.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::str::FromStr; - -use crate::error::{ConfigError, Result}; -use crate::map::Map; -use crate::value::{Value, ValueKind}; - -mod parser; - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum Expression { - Identifier(String), - Child(Box, String), - Subscript(Box, isize), -} - -impl FromStr for Expression { - type Err = ConfigError; - - fn from_str(s: &str) -> Result { - parser::from_str(s).map_err(ConfigError::PathParse) - } -} - -fn sindex_to_uindex(index: isize, len: usize) -> usize { - if index >= 0 { - index as usize - } else { - len - (index.abs() as usize) - } -} - -impl Expression { - pub fn get(self, root: &Value) -> Option<&Value> { - match self { - Self::Identifier(id) => { - match root.kind { - // `x` access on a table is equivalent to: map[x] - ValueKind::Table(ref map) => map.get(&id), - - // all other variants return None - _ => None, - } - } - - Self::Child(expr, key) => { - match expr.get(root) { - Some(value) => { - match value.kind { - // Access on a table is identical to Identifier, it just forwards - ValueKind::Table(ref map) => map.get(&key), - - // all other variants return None - _ => None, - } - } - - _ => None, - } - } - - Self::Subscript(expr, index) => match expr.get(root) { - Some(value) => match value.kind { - ValueKind::Array(ref array) => { - let index = sindex_to_uindex(index, array.len()); - - if index >= array.len() { - None - } else { - Some(&array[index]) - } - } - - _ => None, - }, - - _ => None, - }, - } - } - - pub fn get_mut<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> { - match *self { - Self::Identifier(ref id) => match root.kind { - ValueKind::Table(ref mut map) => map.get_mut(id), - - _ => None, - }, - - Self::Child(ref expr, ref key) => match expr.get_mut(root) { - Some(value) => match value.kind { - ValueKind::Table(ref mut map) => map.get_mut(key), - - _ => None, - }, - - _ => None, - }, - - Self::Subscript(ref expr, index) => match expr.get_mut(root) { - Some(value) => match value.kind { - ValueKind::Array(ref mut array) => { - let index = sindex_to_uindex(index, array.len()); - - if index >= array.len() { - None - } else { - Some(&mut array[index]) - } - } - - _ => None, - }, - - _ => None, - }, - } - } - - pub fn get_mut_forcibly<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> { - match *self { - Self::Identifier(ref id) => match root.kind { - ValueKind::Table(ref mut map) => Some( - map.entry(id.clone()) - .or_insert_with(|| Value::new(None, ValueKind::Nil)), - ), - - _ => None, - }, - - Self::Child(ref expr, ref key) => match expr.get_mut_forcibly(root) { - Some(value) => { - if let ValueKind::Table(ref mut map) = value.kind { - Some( - map.entry(key.clone()) - .or_insert_with(|| Value::new(None, ValueKind::Nil)), - ) - } else { - *value = Map::::new().into(); - - if let ValueKind::Table(ref mut map) = value.kind { - Some( - map.entry(key.clone()) - .or_insert_with(|| Value::new(None, ValueKind::Nil)), - ) - } else { - unreachable!(); - } - } - } - - _ => None, - }, - - Self::Subscript(ref expr, index) => match expr.get_mut_forcibly(root) { - Some(value) => { - match value.kind { - ValueKind::Array(_) => (), - _ => *value = Vec::::new().into(), - } - - match value.kind { - ValueKind::Array(ref mut array) => { - let index = sindex_to_uindex(index, array.len()); - - if index >= array.len() { - array - .resize((index + 1) as usize, Value::new(None, ValueKind::Nil)); - } - - Some(&mut array[index]) - } - - _ => None, - } - } - _ => None, - }, - } - } - - pub fn set(&self, root: &mut Value, value: Value) { - match *self { - Self::Identifier(ref id) => { - // Ensure that root is a table - match root.kind { - ValueKind::Table(_) => {} - - _ => { - *root = Map::::new().into(); - } - } - - match value.kind { - ValueKind::Table(ref incoming_map) => { - // Pull out another table - let target = if let ValueKind::Table(ref mut map) = root.kind { - map.entry(id.clone()) - .or_insert_with(|| Map::::new().into()) - } else { - unreachable!(); - }; - - // Continue the deep merge - for (key, val) in incoming_map { - Self::Identifier(key.clone()).set(target, val.clone()); - } - } - - _ => { - if let ValueKind::Table(ref mut map) = root.kind { - // Just do a simple set - if let Some(existing) = map.get_mut(id) { - *existing = value; - } else { - map.insert(id.clone(), value); - } - } - } - } - } - - Self::Child(ref expr, ref key) => { - if let Some(parent) = expr.get_mut_forcibly(root) { - if !matches!(parent.kind, ValueKind::Table(_)) { - // Didn't find a table. Oh well. Make a table and do this anyway - *parent = Map::::new().into(); - } - Self::Identifier(key.clone()).set(parent, value); - } - } - - Self::Subscript(ref expr, index) => { - if let Some(parent) = expr.get_mut_forcibly(root) { - if !matches!(parent.kind, ValueKind::Array(_)) { - *parent = Vec::::new().into() - } - - if let ValueKind::Array(ref mut array) = parent.kind { - let uindex = sindex_to_uindex(index, array.len()); - if uindex >= array.len() { - array.resize((uindex + 1) as usize, Value::new(None, ValueKind::Nil)); - } - - array[uindex] = value; - } - } - } - } - } -} diff --git a/src/path/parser.rs b/src/path/parser.rs deleted file mode 100644 index 8378121b..00000000 --- a/src/path/parser.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::str::FromStr; - -use nom::{ - branch::alt, - bytes::complete::{is_a, tag}, - character::complete::{char, digit1, space0}, - combinator::{map, map_res, opt, recognize}, - error::ErrorKind, - sequence::{delimited, pair, preceded}, - Err, IResult, -}; - -use crate::path::Expression; - -fn raw_ident(i: &str) -> IResult<&str, String> { - map( - is_a( - "abcdefghijklmnopqrstuvwxyz \ - ABCDEFGHIJKLMNOPQRSTUVWXYZ \ - 0123456789 \ - _-", - ), - ToString::to_string, - )(i) -} - -fn integer(i: &str) -> IResult<&str, isize> { - map_res( - delimited(space0, recognize(pair(opt(tag("-")), digit1)), space0), - FromStr::from_str, - )(i) -} - -fn ident(i: &str) -> IResult<&str, Expression> { - map(raw_ident, Expression::Identifier)(i) -} - -fn postfix<'a>(expr: Expression) -> impl FnMut(&'a str) -> IResult<&'a str, Expression> { - let e2 = expr.clone(); - let child = map(preceded(tag("."), raw_ident), move |id| { - Expression::Child(Box::new(expr.clone()), id) - }); - - let subscript = map(delimited(char('['), integer, char(']')), move |num| { - Expression::Subscript(Box::new(e2.clone()), num) - }); - - alt((child, subscript)) -} - -pub fn from_str(input: &str) -> Result { - match ident(input) { - Ok((mut rem, mut expr)) => { - while !rem.is_empty() { - match postfix(expr)(rem) { - Ok((rem_, expr_)) => { - rem = rem_; - expr = expr_; - } - - // Forward Incomplete and Error - result => { - return result.map(|(_, o)| o).map_err(to_error_kind); - } - } - } - - Ok(expr) - } - - // Forward Incomplete and Error - result => result.map(|(_, o)| o).map_err(to_error_kind), - } -} - -pub fn to_error_kind(e: Err>) -> ErrorKind { - match e { - Err::Incomplete(_) => ErrorKind::Complete, - Err::Failure(e) | Err::Error(e) => e.code, - } -} - -#[cfg(test)] -mod test { - use super::Expression::*; - use super::*; - - #[test] - fn test_id() { - let parsed: Expression = from_str("abcd").unwrap(); - assert_eq!(parsed, Identifier("abcd".into())); - } - - #[test] - fn test_id_dash() { - let parsed: Expression = from_str("abcd-efgh").unwrap(); - assert_eq!(parsed, Identifier("abcd-efgh".into())); - } - - #[test] - fn test_child() { - let parsed: Expression = from_str("abcd.efgh").unwrap(); - let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into()); - - assert_eq!(parsed, expected); - - let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap(); - let expected = Child( - Box::new(Child(Box::new(Identifier("abcd".into())), "efgh".into())), - "ijkl".into(), - ); - - assert_eq!(parsed, expected); - } - - #[test] - fn test_subscript() { - let parsed: Expression = from_str("abcd[12]").unwrap(); - let expected = Subscript(Box::new(Identifier("abcd".into())), 12); - - assert_eq!(parsed, expected); - } - - #[test] - fn test_subscript_neg() { - let parsed: Expression = from_str("abcd[-1]").unwrap(); - let expected = Subscript(Box::new(Identifier("abcd".into())), -1); - - assert_eq!(parsed, expected); - } -} diff --git a/src/ser.rs b/src/ser.rs deleted file mode 100644 index 9ccf7c77..00000000 --- a/src/ser.rs +++ /dev/null @@ -1,720 +0,0 @@ -use std::fmt::Display; - -use serde::ser; - -use crate::error::{ConfigError, Result}; -use crate::value::{Value, ValueKind}; -use crate::Config; - -#[derive(Default, Debug)] -pub struct ConfigSerializer { - keys: Vec<(String, Option)>, - pub output: Config, -} - -impl ConfigSerializer { - fn serialize_primitive(&mut self, value: T) -> Result<()> - where - T: Into + Display, - { - let key = match self.last_key_index_pair() { - Some((key, Some(index))) => format!("{}[{}]", key, index), - Some((key, None)) => key.to_string(), - None => { - return Err(ConfigError::Message(format!( - "key is not found for value {}", - value - ))) - } - }; - - #[allow(deprecated)] - self.output.set(&key, value.into())?; - Ok(()) - } - - fn last_key_index_pair(&self) -> Option<(&str, Option)> { - let len = self.keys.len(); - if len > 0 { - self.keys - .get(len - 1) - .map(|&(ref key, opt)| (key.as_str(), opt)) - } else { - None - } - } - - fn inc_last_key_index(&mut self) -> Result<()> { - let len = self.keys.len(); - if len > 0 { - self.keys - .get_mut(len - 1) - .map(|pair| pair.1 = pair.1.map(|i| i + 1).or(Some(0))) - .ok_or_else(|| { - ConfigError::Message(format!("last key is not found in {} keys", len)) - }) - } else { - Err(ConfigError::Message("keys is empty".to_string())) - } - } - - fn make_full_key(&self, key: &str) -> String { - let len = self.keys.len(); - if len > 0 { - if let Some(&(ref prev_key, index)) = self.keys.get(len - 1) { - return if let Some(index) = index { - format!("{}[{}].{}", prev_key, index, key) - } else { - format!("{}.{}", prev_key, key) - }; - } - } - key.to_string() - } - - fn push_key(&mut self, key: &str) { - let full_key = self.make_full_key(key); - self.keys.push((full_key, None)); - } - - fn pop_key(&mut self) -> Option<(String, Option)> { - self.keys.pop() - } -} - -impl<'a> ser::Serializer for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - type SerializeSeq = Self; - type SerializeTuple = Self; - type SerializeTupleStruct = Self; - type SerializeTupleVariant = Self; - type SerializeMap = Self; - type SerializeStruct = Self; - type SerializeStructVariant = Self; - - fn serialize_bool(self, v: bool) -> Result { - self.serialize_primitive(v) - } - - fn serialize_i8(self, v: i8) -> Result { - self.serialize_i64(v.into()) - } - - fn serialize_i16(self, v: i16) -> Result { - self.serialize_i64(v.into()) - } - - fn serialize_i32(self, v: i32) -> Result { - self.serialize_i64(v.into()) - } - - fn serialize_i64(self, v: i64) -> Result { - self.serialize_primitive(v) - } - - fn serialize_u8(self, v: u8) -> Result { - self.serialize_u64(v.into()) - } - - fn serialize_u16(self, v: u16) -> Result { - self.serialize_u64(v.into()) - } - - fn serialize_u32(self, v: u32) -> Result { - self.serialize_u64(v.into()) - } - - fn serialize_u64(self, v: u64) -> Result { - if v > (i64::max_value() as u64) { - Err(ConfigError::Message(format!( - "value {} is greater than the max {}", - v, - i64::max_value() - ))) - } else { - self.serialize_i64(v as i64) - } - } - - fn serialize_f32(self, v: f32) -> Result { - self.serialize_f64(v.into()) - } - - fn serialize_f64(self, v: f64) -> Result { - self.serialize_primitive(v) - } - - fn serialize_char(self, v: char) -> Result { - self.serialize_primitive(v.to_string()) - } - - fn serialize_str(self, v: &str) -> Result { - self.serialize_primitive(v.to_string()) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - use serde::ser::SerializeSeq; - let mut seq = self.serialize_seq(Some(v.len()))?; - for byte in v { - seq.serialize_element(byte)?; - } - seq.end() - } - - fn serialize_none(self) -> Result { - self.serialize_unit() - } - - fn serialize_some(self, value: &T) -> Result - where - T: ?Sized + ser::Serialize, - { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - self.serialize_primitive(Value::from(ValueKind::Nil)) - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result { - self.serialize_unit() - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _variant_index: u32, - variant: &'static str, - ) -> Result { - self.serialize_str(variant) - } - - fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result - where - T: ?Sized + ser::Serialize, - { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _variant_index: u32, - variant: &'static str, - value: &T, - ) -> Result - where - T: ?Sized + ser::Serialize, - { - self.push_key(variant); - value.serialize(&mut *self)?; - self.pop_key(); - Ok(()) - } - - fn serialize_seq(self, _len: Option) -> Result { - Ok(self) - } - - fn serialize_tuple(self, len: usize) -> Result { - self.serialize_seq(Some(len)) - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - len: usize, - ) -> Result { - self.serialize_seq(Some(len)) - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _variant_index: u32, - variant: &'static str, - _len: usize, - ) -> Result { - self.push_key(variant); - Ok(self) - } - - fn serialize_map(self, _len: Option) -> Result { - Ok(self) - } - - fn serialize_struct(self, _name: &'static str, len: usize) -> Result { - self.serialize_map(Some(len)) - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _variant_index: u32, - variant: &'static str, - _len: usize, - ) -> Result { - self.push_key(variant); - Ok(self) - } -} - -impl<'a> ser::SerializeSeq for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_element(&mut self, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.inc_last_key_index()?; - value.serialize(&mut **self)?; - Ok(()) - } - - fn end(self) -> Result { - Ok(()) - } -} - -impl<'a> ser::SerializeTuple for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_element(&mut self, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.inc_last_key_index()?; - value.serialize(&mut **self)?; - Ok(()) - } - - fn end(self) -> Result { - Ok(()) - } -} - -impl<'a> ser::SerializeTupleStruct for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_field(&mut self, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.inc_last_key_index()?; - value.serialize(&mut **self)?; - Ok(()) - } - - fn end(self) -> Result { - Ok(()) - } -} - -impl<'a> ser::SerializeTupleVariant for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_field(&mut self, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.inc_last_key_index()?; - value.serialize(&mut **self)?; - Ok(()) - } - - fn end(self) -> Result { - self.pop_key(); - Ok(()) - } -} - -impl<'a> ser::SerializeMap for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_key(&mut self, key: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - let key_serializer = StringKeySerializer; - let key = key.serialize(key_serializer)?; - self.push_key(&key); - Ok(()) - } - - fn serialize_value(&mut self, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - value.serialize(&mut **self)?; - self.pop_key(); - Ok(()) - } - - fn end(self) -> Result { - Ok(()) - } -} - -impl<'a> ser::SerializeStruct for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.push_key(key); - value.serialize(&mut **self)?; - self.pop_key(); - Ok(()) - } - - fn end(self) -> Result { - Ok(()) - } -} - -impl<'a> ser::SerializeStructVariant for &'a mut ConfigSerializer { - type Ok = (); - type Error = ConfigError; - - fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - self.push_key(key); - value.serialize(&mut **self)?; - self.pop_key(); - Ok(()) - } - - fn end(self) -> Result { - self.pop_key(); - Ok(()) - } -} - -pub struct StringKeySerializer; - -impl ser::Serializer for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - type SerializeSeq = Self; - type SerializeTuple = Self; - type SerializeTupleStruct = Self; - type SerializeTupleVariant = Self; - type SerializeMap = Self; - type SerializeStruct = Self; - type SerializeStructVariant = Self; - - fn serialize_bool(self, v: bool) -> Result { - Ok(v.to_string()) - } - - fn serialize_i8(self, v: i8) -> Result { - Ok(v.to_string()) - } - - fn serialize_i16(self, v: i16) -> Result { - Ok(v.to_string()) - } - - fn serialize_i32(self, v: i32) -> Result { - Ok(v.to_string()) - } - - fn serialize_i64(self, v: i64) -> Result { - Ok(v.to_string()) - } - - fn serialize_u8(self, v: u8) -> Result { - Ok(v.to_string()) - } - - fn serialize_u16(self, v: u16) -> Result { - Ok(v.to_string()) - } - - fn serialize_u32(self, v: u32) -> Result { - Ok(v.to_string()) - } - - fn serialize_u64(self, v: u64) -> Result { - Ok(v.to_string()) - } - - fn serialize_f32(self, v: f32) -> Result { - Ok(v.to_string()) - } - - fn serialize_f64(self, v: f64) -> Result { - Ok(v.to_string()) - } - - fn serialize_char(self, v: char) -> Result { - Ok(v.to_string()) - } - - fn serialize_str(self, v: &str) -> Result { - Ok(v.to_string()) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - Ok(String::from_utf8_lossy(v).to_string()) - } - - fn serialize_none(self) -> Result { - self.serialize_unit() - } - - fn serialize_some(self, value: &T) -> Result - where - T: ?Sized + ser::Serialize, - { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - Ok(String::new()) - } - - fn serialize_unit_struct(self, _name: &str) -> Result { - self.serialize_unit() - } - - fn serialize_unit_variant( - self, - _name: &str, - _variant_index: u32, - variant: &str, - ) -> Result { - Ok(variant.to_string()) - } - - fn serialize_newtype_struct(self, _name: &str, value: &T) -> Result - where - T: ?Sized + ser::Serialize, - { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _name: &str, - _variant_index: u32, - _variant: &str, - value: &T, - ) -> Result - where - T: ?Sized + ser::Serialize, - { - value.serialize(self) - } - - fn serialize_seq(self, _len: Option) -> Result { - Err(ConfigError::Message( - "seq can't serialize to string key".to_string(), - )) - } - - fn serialize_tuple(self, _len: usize) -> Result { - Err(ConfigError::Message( - "tuple can't serialize to string key".to_string(), - )) - } - - fn serialize_tuple_struct(self, name: &str, _len: usize) -> Result { - Err(ConfigError::Message(format!( - "tuple struct {} can't serialize to string key", - name - ))) - } - - fn serialize_tuple_variant( - self, - name: &str, - _variant_index: u32, - variant: &str, - _len: usize, - ) -> Result { - Err(ConfigError::Message(format!( - "tuple variant {}::{} can't serialize to string key", - name, variant - ))) - } - - fn serialize_map(self, _len: Option) -> Result { - Err(ConfigError::Message( - "map can't serialize to string key".to_string(), - )) - } - - fn serialize_struct(self, name: &str, _len: usize) -> Result { - Err(ConfigError::Message(format!( - "struct {} can't serialize to string key", - name - ))) - } - - fn serialize_struct_variant( - self, - name: &str, - _variant_index: u32, - variant: &str, - _len: usize, - ) -> Result { - Err(ConfigError::Message(format!( - "struct variant {}::{} can't serialize to string key", - name, variant - ))) - } -} - -impl ser::SerializeSeq for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_element(&mut self, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeTuple for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_element(&mut self, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeTupleStruct for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_field(&mut self, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeTupleVariant for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_field(&mut self, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeMap for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_key(&mut self, _key: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn serialize_value(&mut self, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeStruct for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -impl ser::SerializeStructVariant for StringKeySerializer { - type Ok = String; - type Error = ConfigError; - - fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> - where - T: ?Sized + ser::Serialize, - { - unreachable!() - } - - fn end(self) -> Result { - unreachable!() - } -} - -#[cfg(test)] -mod test { - use super::*; - use serde::{Deserialize, Serialize}; - - #[test] - fn test_struct() { - #[derive(Debug, Serialize, Deserialize, PartialEq)] - struct Test { - int: u32, - seq: Vec, - } - - let test = Test { - int: 1, - seq: vec!["a".to_string(), "b".to_string()], - }; - let config = Config::try_from(&test).unwrap(); - - let actual: Test = config.try_deserialize().unwrap(); - assert_eq!(test, actual); - } -} diff --git a/src/source.rs b/src/source.rs deleted file mode 100644 index 94f1c357..00000000 --- a/src/source.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::fmt::Debug; -use std::str::FromStr; - -use async_trait::async_trait; - -use crate::error::Result; -use crate::map::Map; -use crate::path; -use crate::value::{Value, ValueKind}; - -/// Describes a generic _source_ of configuration properties. -pub trait Source: Debug { - fn clone_into_box(&self) -> Box; - - /// Collect all configuration properties available from this source and return - /// a Map. - fn collect(&self) -> Result>; - - /// Collects all configuration properties to a provided cache. - fn collect_to(&self, cache: &mut Value) -> Result<()> { - self.collect()? - .iter() - .for_each(|(key, val)| set_value(cache, key, val)); - - Ok(()) - } -} - -fn set_value(cache: &mut Value, key: &str, value: &Value) { - match path::Expression::from_str(key) { - // Set using the path - Ok(expr) => expr.set(cache, value.clone()), - - // Set diretly anyway - _ => path::Expression::Identifier(key.to_string()).set(cache, value.clone()), - } -} - -/// Describes a generic _source_ of configuration properties capable of using an async runtime. -/// -/// At the moment this library does not implement it, although it allows using its implementations -/// within builders. Due to the scattered landscape of asynchronous runtimes, it is impossible to -/// cater to all needs with one implementation. Also, this trait might be most useful with remote -/// configuration sources, reachable via the network, probably using HTTP protocol. Numerous HTTP -/// libraries exist, making it even harder to find one implementation that rules them all. -/// -/// For those reasons, it is left to other crates to implement runtime-specific or proprietary -/// details. -/// -/// It is advised to use `async_trait` crate while implementing this trait. -/// -/// See examples for sample implementation. -#[async_trait] -pub trait AsyncSource: Debug + Sync { - // Sync is supertrait due to https://docs.rs/async-trait/0.1.50/async_trait/index.html#dyn-traits - - /// Collects all configuration properties available from this source and return - /// a Map as an async operations. - async fn collect(&self) -> Result>; - - /// Collects all configuration properties to a provided cache. - async fn collect_to(&self, cache: &mut Value) -> Result<()> { - self.collect() - .await? - .iter() - .for_each(|(key, val)| set_value(cache, key, val)); - - Ok(()) - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.to_owned() - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - self.clone_into_box() - } -} - -impl Source for Vec> { - fn clone_into_box(&self) -> Box { - Box::new((*self).clone()) - } - - fn collect(&self) -> Result> { - let mut cache: Value = Map::::new().into(); - - for source in self { - source.collect_to(&mut cache)?; - } - - if let ValueKind::Table(table) = cache.kind { - Ok(table) - } else { - unreachable!(); - } - } -} - -impl Source for [Box] { - fn clone_into_box(&self) -> Box { - Box::new(self.to_owned()) - } - - fn collect(&self) -> Result> { - let mut cache: Value = Map::::new().into(); - - for source in self { - source.collect_to(&mut cache)?; - } - - if let ValueKind::Table(table) = cache.kind { - Ok(table) - } else { - unreachable!(); - } - } -} - -impl Source for Vec -where - T: Source + Sync + Send + Clone + 'static, -{ - fn clone_into_box(&self) -> Box { - Box::new((*self).clone()) - } - - fn collect(&self) -> Result> { - let mut cache: Value = Map::::new().into(); - - for source in self { - source.collect_to(&mut cache)?; - } - - if let ValueKind::Table(table) = cache.kind { - Ok(table) - } else { - unreachable!(); - } - } -} diff --git a/src/source/format.rs b/src/source/format.rs new file mode 100644 index 00000000..6486f806 --- /dev/null +++ b/src/source/format.rs @@ -0,0 +1,35 @@ +use crate::element::IntoConfigElement; + +use super::SourceError; + +pub trait FormatParser: std::fmt::Debug { + type Output: IntoConfigElement + std::fmt::Debug + Sized; + + fn parse(buffer: &str) -> Result; +} + +#[cfg(feature = "json")] +#[derive(Debug)] +pub struct JsonFormatParser; + +#[cfg(feature = "json")] +impl FormatParser for JsonFormatParser { + type Output = serde_json::Value; + + fn parse(buffer: &str) -> Result { + serde_json::from_str(buffer).map_err(SourceError::JsonParserError) + } +} + +#[cfg(feature = "toml")] +#[derive(Debug)] +pub struct TomlFormatParser; + +#[cfg(feature = "toml")] +impl FormatParser for TomlFormatParser { + type Output = toml::Value; + + fn parse(buffer: &str) -> Result { + toml::from_str(buffer).map_err(SourceError::TomlParserError) + } +} diff --git a/src/source/mod.rs b/src/source/mod.rs new file mode 100644 index 00000000..1bee2cc2 --- /dev/null +++ b/src/source/mod.rs @@ -0,0 +1,56 @@ +use crate::object::ConfigObject; + +mod format; +mod string; + +pub use crate::source::format::FormatParser; +pub use crate::source::format::JsonFormatParser; +pub use crate::source::string::StringSource; + +pub trait ConfigSource: std::fmt::Debug { + fn load(&self) -> Result; +} + +#[derive(Debug, thiserror::Error)] +pub enum SourceError { + #[error("IO Error")] + Io(#[from] std::io::Error), + + #[cfg(feature = "json")] + #[error("JSON Parser error")] + JsonParserError(#[from] serde_json::Error), + + #[cfg(feature = "json")] + #[error("JSON load error")] + JsonLoadError(#[from] crate::element::json::JsonIntoConfigElementError), + + #[cfg(feature = "toml")] + #[error("TOML Parser error")] + TomlParserError(#[from] toml::de::Error), + + #[cfg(feature = "toml")] + #[error("TOML load error")] + TomlLoadError(#[from] crate::element::toml::TomlIntoConfigElementError), +} + +#[cfg(test)] +pub(crate) mod test_source { + use crate::description::ConfigSourceDescription; + use crate::element::ConfigElement; + use crate::object::ConfigObject; + use crate::source::ConfigSource; + + use super::SourceError; + + #[derive(Debug)] + pub(crate) struct TestSource(pub(crate) ConfigElement); + + impl ConfigSource for TestSource { + fn load(&self) -> Result { + Ok(ConfigObject::new( + self.0.clone(), + ConfigSourceDescription::Unknown, + )) + } + } +} diff --git a/src/source/string.rs b/src/source/string.rs new file mode 100644 index 00000000..b4e7efdf --- /dev/null +++ b/src/source/string.rs @@ -0,0 +1,51 @@ +use crate::description::ConfigSourceDescription; +use crate::element::IntoConfigElement; +use crate::object::ConfigObject; +use crate::source::format::FormatParser; +use crate::ConfigSource; + +use super::SourceError; + +#[derive(Debug)] +pub struct StringSource { + source: String, + _pd: std::marker::PhantomData

, +} + +impl StringSource

{ + pub fn new(source: String) -> Result { + Ok(StringSource { + source, + _pd: std::marker::PhantomData, + }) + } +} + +impl

ConfigSource for StringSource

+where + P: FormatParser + std::fmt::Debug, + SourceError: From<<

::Output as IntoConfigElement>::Error>, +{ + fn load(&self) -> Result { + let element = P::parse(&self.source)?; + let element = element.into_config_element()?; + + let desc = ConfigSourceDescription::Custom("String".to_string()); + Ok(ConfigObject::new(element, desc)) + } +} + +#[cfg(test)] +mod test_source_impl { + #[cfg(feature = "json")] + #[test] + fn test_json_string_source() { + use super::*; + + let source = "{}"; + + let source = + StringSource::::new(source.to_string()).unwrap(); + let _object = source.load().unwrap(); + } +} diff --git a/src/value.rs b/src/value.rs deleted file mode 100644 index c3dad104..00000000 --- a/src/value.rs +++ /dev/null @@ -1,864 +0,0 @@ -use std::fmt; -use std::fmt::Display; - -use serde::de::{Deserialize, Deserializer, Visitor}; - -use crate::error::{ConfigError, Result, Unexpected}; -use crate::map::Map; - -/// Underlying kind of the configuration value. -/// -/// Standard operations on a `Value` by users of this crate do not require -/// knowledge of `ValueKind`. Introspection of underlying kind is only required -/// when the configuration values are unstructured or do not have known types. -#[derive(Debug, Clone, PartialEq)] -pub enum ValueKind { - Nil, - Boolean(bool), - I64(i64), - I128(i128), - U64(u64), - U128(u128), - Float(f64), - String(String), - Table(Table), - Array(Array), -} - -pub type Array = Vec; -pub type Table = Map; - -impl Default for ValueKind { - fn default() -> Self { - Self::Nil - } -} - -impl From> for ValueKind -where - T: Into, -{ - fn from(value: Option) -> Self { - match value { - Some(value) => value.into(), - None => Self::Nil, - } - } -} - -impl From for ValueKind { - fn from(value: String) -> Self { - Self::String(value) - } -} - -impl<'a> From<&'a str> for ValueKind { - fn from(value: &'a str) -> Self { - Self::String(value.into()) - } -} - -impl From for ValueKind { - fn from(value: i8) -> Self { - Self::I64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: i16) -> Self { - Self::I64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: i32) -> Self { - Self::I64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: i64) -> Self { - Self::I64(value) - } -} - -impl From for ValueKind { - fn from(value: i128) -> Self { - Self::I128(value) - } -} - -impl From for ValueKind { - fn from(value: u8) -> Self { - Self::U64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: u16) -> Self { - Self::U64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: u32) -> Self { - Self::U64(value.into()) - } -} - -impl From for ValueKind { - fn from(value: u64) -> Self { - Self::U64(value) - } -} - -impl From for ValueKind { - fn from(value: u128) -> Self { - Self::U128(value) - } -} - -impl From for ValueKind { - fn from(value: f64) -> Self { - Self::Float(value) - } -} - -impl From for ValueKind { - fn from(value: bool) -> Self { - Self::Boolean(value) - } -} - -impl From> for ValueKind -where - T: Into, -{ - fn from(values: Map) -> Self { - let t = values.into_iter().map(|(k, v)| (k, v.into())).collect(); - Self::Table(t) - } -} - -impl From> for ValueKind -where - T: Into, -{ - fn from(values: Vec) -> Self { - Self::Array(values.into_iter().map(T::into).collect()) - } -} - -impl Display for ValueKind { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Self::String(ref value) => write!(f, "{}", value), - Self::Boolean(value) => write!(f, "{}", value), - Self::I64(value) => write!(f, "{}", value), - Self::I128(value) => write!(f, "{}", value), - Self::U64(value) => write!(f, "{}", value), - Self::U128(value) => write!(f, "{}", value), - Self::Float(value) => write!(f, "{}", value), - Self::Nil => write!(f, "nil"), - Self::Table(ref table) => write!(f, "{{ {} }}", { - table - .iter() - .map(|(k, v)| format!("{} => {}, ", k, v)) - .collect::() - }), - Self::Array(ref array) => write!(f, "{:?}", { - array.iter().map(|e| format!("{}, ", e)).collect::() - }), - } - } -} - -/// A configuration value. -#[derive(Default, Debug, Clone, PartialEq)] -pub struct Value { - /// A description of the original location of the value. - /// - /// A Value originating from a File might contain: - /// ```text - /// Settings.toml - /// ``` - /// - /// A Value originating from the environment would contain: - /// ```text - /// the envrionment - /// ``` - /// - /// A Value originating from a remote source might contain: - /// ```text - /// etcd+http://127.0.0.1:2379 - /// ``` - origin: Option, - - /// Underlying kind of the configuration value. - pub kind: ValueKind, -} - -impl Value { - /// Create a new value instance that will remember its source uri. - pub fn new(origin: Option<&String>, kind: V) -> Self - where - V: Into, - { - Self { - origin: origin.cloned(), - kind: kind.into(), - } - } - - /// Attempt to deserialize this value into the requested type. - pub fn try_deserialize<'de, T: Deserialize<'de>>(self) -> Result { - T::deserialize(self) - } - - /// Returns `self` as a bool, if possible. - // FIXME: Should this not be `try_into_*` ? - pub fn into_bool(self) -> Result { - match self.kind { - ValueKind::Boolean(value) => Ok(value), - ValueKind::I64(value) => Ok(value != 0), - ValueKind::I128(value) => Ok(value != 0), - ValueKind::U64(value) => Ok(value != 0), - ValueKind::U128(value) => Ok(value != 0), - ValueKind::Float(value) => Ok(value != 0.0), - - ValueKind::String(ref value) => { - match value.to_lowercase().as_ref() { - "1" | "true" | "on" | "yes" => Ok(true), - "0" | "false" | "off" | "no" => Ok(false), - - // Unexpected string value - s => Err(ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.into()), - "a boolean", - )), - } - } - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "a boolean", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "a boolean", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "a boolean", - )), - } - } - - /// Returns `self` into an i64, if possible. - // FIXME: Should this not be `try_into_*` ? - pub fn into_int(self) -> Result { - match self.kind { - ValueKind::I64(value) => Ok(value), - ValueKind::I128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I128(value), - "an signed 64 bit or less integer", - )), - ValueKind::U64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U64(value), - "an signed 64 bit or less integer", - )), - ValueKind::U128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U128(value), - "an signed 64 bit or less integer", - )), - - ValueKind::String(ref s) => { - match s.to_lowercase().as_ref() { - "true" | "on" | "yes" => Ok(1), - "false" | "off" | "no" => Ok(0), - _ => { - s.parse().map_err(|_| { - // Unexpected string - ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.clone()), - "an integer", - ) - }) - } - } - } - - ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), - ValueKind::Float(value) => Ok(value.round() as i64), - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "an integer", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "an integer", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "an integer", - )), - } - } - - /// Returns `self` into an i128, if possible. - pub fn into_int128(self) -> Result { - match self.kind { - ValueKind::I64(value) => Ok(value.into()), - ValueKind::I128(value) => Ok(value), - ValueKind::U64(value) => Ok(value.into()), - ValueKind::U128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U128(value), - "an signed 128 bit integer", - )), - - ValueKind::String(ref s) => { - match s.to_lowercase().as_ref() { - "true" | "on" | "yes" => Ok(1), - "false" | "off" | "no" => Ok(0), - _ => { - s.parse().map_err(|_| { - // Unexpected string - ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.clone()), - "an integer", - ) - }) - } - } - } - - ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), - ValueKind::Float(value) => Ok(value.round() as i128), - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "an integer", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "an integer", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "an integer", - )), - } - } - - /// Returns `self` into an u64, if possible. - // FIXME: Should this not be `try_into_*` ? - pub fn into_uint(self) -> Result { - match self.kind { - ValueKind::U64(value) => Ok(value), - ValueKind::U128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U128(value), - "an unsigned 64 bit or less integer", - )), - ValueKind::I64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I64(value), - "an unsigned 64 bit or less integer", - )), - ValueKind::I128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I128(value), - "an unsigned 64 bit or less integer", - )), - - ValueKind::String(ref s) => { - match s.to_lowercase().as_ref() { - "true" | "on" | "yes" => Ok(1), - "false" | "off" | "no" => Ok(0), - _ => { - s.parse().map_err(|_| { - // Unexpected string - ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.clone()), - "an integer", - ) - }) - } - } - } - - ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), - ValueKind::Float(value) => Ok(value.round() as u64), - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "an integer", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "an integer", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "an integer", - )), - } - } - - /// Returns `self` into an u128, if possible. - pub fn into_uint128(self) -> Result { - match self.kind { - ValueKind::U64(value) => Ok(value.into()), - ValueKind::U128(value) => Ok(value), - ValueKind::I64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I64(value), - "an unsigned 128 bit or less integer", - )), - ValueKind::I128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I128(value), - "an unsigned 128 bit or less integer", - )), - - ValueKind::String(ref s) => { - match s.to_lowercase().as_ref() { - "true" | "on" | "yes" => Ok(1), - "false" | "off" | "no" => Ok(0), - _ => { - s.parse().map_err(|_| { - // Unexpected string - ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.clone()), - "an integer", - ) - }) - } - } - } - - ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }), - ValueKind::Float(value) => Ok(value.round() as u128), - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "an integer", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "an integer", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "an integer", - )), - } - } - - /// Returns `self` into a f64, if possible. - // FIXME: Should this not be `try_into_*` ? - pub fn into_float(self) -> Result { - match self.kind { - ValueKind::Float(value) => Ok(value), - - ValueKind::String(ref s) => { - match s.to_lowercase().as_ref() { - "true" | "on" | "yes" => Ok(1.0), - "false" | "off" | "no" => Ok(0.0), - _ => { - s.parse().map_err(|_| { - // Unexpected string - ConfigError::invalid_type( - self.origin.clone(), - Unexpected::Str(s.clone()), - "a floating point", - ) - }) - } - } - } - - ValueKind::I64(value) => Ok(value as f64), - ValueKind::I128(value) => Ok(value as f64), - ValueKind::U64(value) => Ok(value as f64), - ValueKind::U128(value) => Ok(value as f64), - ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }), - - // Unexpected type - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "a floating point", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "a floating point", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "a floating point", - )), - } - } - - /// Returns `self` into a string, if possible. - // FIXME: Should this not be `try_into_*` ? - pub fn into_string(self) -> Result { - match self.kind { - ValueKind::String(value) => Ok(value), - - ValueKind::Boolean(value) => Ok(value.to_string()), - ValueKind::I64(value) => Ok(value.to_string()), - ValueKind::I128(value) => Ok(value.to_string()), - ValueKind::U64(value) => Ok(value.to_string()), - ValueKind::U128(value) => Ok(value.to_string()), - ValueKind::Float(value) => Ok(value.to_string()), - - // Cannot convert - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "a string", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "a string", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "a string", - )), - } - } - - /// Returns `self` into an array, if possible - // FIXME: Should this not be `try_into_*` ? - pub fn into_array(self) -> Result> { - match self.kind { - ValueKind::Array(value) => Ok(value), - - // Cannot convert - ValueKind::Float(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Float(value), - "an array", - )), - ValueKind::String(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Str(value), - "an array", - )), - ValueKind::I64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I64(value), - "an array", - )), - ValueKind::I128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I128(value), - "an array", - )), - ValueKind::U64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U64(value), - "an array", - )), - ValueKind::U128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U128(value), - "an array", - )), - ValueKind::Boolean(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Bool(value), - "an array", - )), - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "an array", - )), - ValueKind::Table(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Map, - "an array", - )), - } - } - - /// If the `Value` is a Table, returns the associated Map. - // FIXME: Should this not be `try_into_*` ? - pub fn into_table(self) -> Result> { - match self.kind { - ValueKind::Table(value) => Ok(value), - - // Cannot convert - ValueKind::Float(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Float(value), - "a map", - )), - ValueKind::String(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Str(value), - "a map", - )), - ValueKind::I64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I64(value), - "a map", - )), - ValueKind::I128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::I128(value), - "a map", - )), - ValueKind::U64(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U64(value), - "a map", - )), - ValueKind::U128(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::U128(value), - "a map", - )), - ValueKind::Boolean(value) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Bool(value), - "a map", - )), - ValueKind::Nil => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Unit, - "a map", - )), - ValueKind::Array(_) => Err(ConfigError::invalid_type( - self.origin, - Unexpected::Seq, - "a map", - )), - } - } -} - -impl<'de> Deserialize<'de> for Value { - #[inline] - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: Deserializer<'de>, - { - struct ValueVisitor; - - impl<'de> Visitor<'de> for ValueVisitor { - type Value = Value; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("any valid configuration value") - } - - #[inline] - fn visit_bool(self, value: bool) -> ::std::result::Result { - Ok(value.into()) - } - - #[inline] - fn visit_i8(self, value: i8) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_i16(self, value: i16) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_i32(self, value: i32) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_i64(self, value: i64) -> ::std::result::Result { - Ok(value.into()) - } - - #[inline] - fn visit_i128(self, value: i128) -> ::std::result::Result { - Ok(value.into()) - } - - #[inline] - fn visit_u8(self, value: u8) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_u16(self, value: u16) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_u32(self, value: u32) -> ::std::result::Result { - Ok((i64::from(value)).into()) - } - - #[inline] - fn visit_u64(self, value: u64) -> ::std::result::Result { - // FIXME: This is bad - Ok((value as i64).into()) - } - - #[inline] - fn visit_u128(self, value: u128) -> ::std::result::Result { - // FIXME: This is bad - Ok((value as i128).into()) - } - - #[inline] - fn visit_f64(self, value: f64) -> ::std::result::Result { - Ok(value.into()) - } - - #[inline] - fn visit_str(self, value: &str) -> ::std::result::Result - where - E: ::serde::de::Error, - { - self.visit_string(String::from(value)) - } - - #[inline] - fn visit_string(self, value: String) -> ::std::result::Result { - Ok(value.into()) - } - - #[inline] - fn visit_none(self) -> ::std::result::Result { - Ok(Value::new(None, ValueKind::Nil)) - } - - #[inline] - fn visit_some(self, deserializer: D) -> ::std::result::Result - where - D: Deserializer<'de>, - { - Deserialize::deserialize(deserializer) - } - - #[inline] - fn visit_unit(self) -> ::std::result::Result { - Ok(Value::new(None, ValueKind::Nil)) - } - - #[inline] - fn visit_seq(self, mut visitor: V) -> ::std::result::Result - where - V: ::serde::de::SeqAccess<'de>, - { - let mut vec = Array::new(); - - while let Some(elem) = visitor.next_element()? { - vec.push(elem); - } - - Ok(vec.into()) - } - - fn visit_map(self, mut visitor: V) -> ::std::result::Result - where - V: ::serde::de::MapAccess<'de>, - { - let mut values = Table::new(); - - while let Some((key, value)) = visitor.next_entry()? { - values.insert(key, value); - } - - Ok(values.into()) - } - } - - deserializer.deserialize_any(ValueVisitor) - } -} - -impl From for Value -where - T: Into, -{ - fn from(value: T) -> Self { - Self { - origin: None, - kind: value.into(), - } - } -} - -impl Display for Value { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.kind) - } -} - -#[cfg(test)] -mod tests { - use super::ValueKind; - use crate::Config; - use crate::File; - use crate::FileFormat; - - #[test] - fn test_i64() { - let c = Config::builder() - .add_source(File::new("tests/types/i64.toml", FileFormat::Toml)) - .build() - .unwrap(); - - assert!(std::matches!(c.cache.kind, ValueKind::Table(_))); - let v = match c.cache.kind { - ValueKind::Table(t) => t, - _ => unreachable!(), - }; - - let value = v.get("value").unwrap(); - assert!( - std::matches!(value.kind, ValueKind::I64(120)), - "Is not a i64(120): {:?}", - value.kind - ); - } -} diff --git a/tests/Settings-invalid.hjson b/tests/Settings-invalid.hjson deleted file mode 100644 index 7e31ec3d..00000000 --- a/tests/Settings-invalid.hjson +++ /dev/null @@ -1,4 +0,0 @@ -{ - ok: true, - error -} diff --git a/tests/Settings-invalid.ini b/tests/Settings-invalid.ini deleted file mode 100644 index f2b8d9b0..00000000 --- a/tests/Settings-invalid.ini +++ /dev/null @@ -1,2 +0,0 @@ -ok : true, -error diff --git a/tests/Settings-invalid.json b/tests/Settings-invalid.json deleted file mode 100644 index ba2d7cba..00000000 --- a/tests/Settings-invalid.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "ok": true, - "error" -} diff --git a/tests/Settings-invalid.json5 b/tests/Settings-invalid.json5 deleted file mode 100644 index 7e97bc10..00000000 --- a/tests/Settings-invalid.json5 +++ /dev/null @@ -1,4 +0,0 @@ -{ - ok: true - error -} diff --git a/tests/Settings-invalid.ron b/tests/Settings-invalid.ron deleted file mode 100644 index 0f41c5cd..00000000 --- a/tests/Settings-invalid.ron +++ /dev/null @@ -1,4 +0,0 @@ -( - ok: true, - error -) diff --git a/tests/Settings-invalid.toml b/tests/Settings-invalid.toml deleted file mode 100644 index 4d159a4c..00000000 --- a/tests/Settings-invalid.toml +++ /dev/null @@ -1,2 +0,0 @@ -ok = true -error = tru diff --git a/tests/Settings-invalid.yaml b/tests/Settings-invalid.yaml deleted file mode 100644 index 070ff1b7..00000000 --- a/tests/Settings-invalid.yaml +++ /dev/null @@ -1,2 +0,0 @@ -ok: true -error false diff --git a/tests/Settings-production.toml b/tests/Settings-production.toml deleted file mode 100644 index 6545b4cc..00000000 --- a/tests/Settings-production.toml +++ /dev/null @@ -1,8 +0,0 @@ -debug = false -production = true - -[place] -rating = 4.9 - -[place.creator] -name = "Somebody New" diff --git a/tests/Settings.hjson b/tests/Settings.hjson deleted file mode 100644 index 9810e04d..00000000 --- a/tests/Settings.hjson +++ /dev/null @@ -1,18 +0,0 @@ -{ - debug: true - production: false - arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - place: { - name: Torre di Pisa - longitude: 43.7224985 - latitude: 10.3970522 - favorite: false - reviews: 3866 - rating: 4.5 - creator: { - name: John Smith - username: jsmith - email: jsmith@localhost - } - } -} diff --git a/tests/Settings.ini b/tests/Settings.ini deleted file mode 100644 index 16badd47..00000000 --- a/tests/Settings.ini +++ /dev/null @@ -1,9 +0,0 @@ -debug = true -production = false -[place] -name = Torre di Pisa -longitude = 43.7224985 -latitude = 10.3970522 -favorite = false -reviews = 3866 -rating = 4.5 diff --git a/tests/Settings.json b/tests/Settings.json deleted file mode 100644 index babb0ba9..00000000 --- a/tests/Settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "debug": true, - "debug_json": true, - "production": false, - "arr": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - "place": { - "name": "Torre di Pisa", - "longitude": 43.7224985, - "latitude": 10.3970522, - "favorite": false, - "reviews": 3866, - "rating": 4.5, - "creator": { - "name": "John Smith", - "username": "jsmith", - "email": "jsmith@localhost" - } - } -} diff --git a/tests/Settings.json5 b/tests/Settings.json5 deleted file mode 100644 index 4c93e421..00000000 --- a/tests/Settings.json5 +++ /dev/null @@ -1,20 +0,0 @@ -{ - // c - /* c */ - debug: true, - production: false, - arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,], - place: { - name: 'Torre di Pisa', - longitude: 43.7224985, - latitude: 10.3970522, - favorite: false, - reviews: 3866, - rating: 4.5, - creator: { - name: "John Smith", - "username": "jsmith", - "email": "jsmith@localhost", - } - } -} diff --git a/tests/Settings.ron b/tests/Settings.ron deleted file mode 100644 index 7882840a..00000000 --- a/tests/Settings.ron +++ /dev/null @@ -1,20 +0,0 @@ -( - debug: true, - production: false, - arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - place: ( - initials: ('T', 'P'), - name: "Torre di Pisa", - longitude: 43.7224985, - latitude: 10.3970522, - favorite: false, - reviews: 3866, - rating: Some(4.5), - telephone: None, - creator: { - "name": "John Smith", - "username": "jsmith", - "email": "jsmith@localhost" - } - ) -) diff --git a/tests/Settings.toml b/tests/Settings.toml deleted file mode 100644 index bb538087..00000000 --- a/tests/Settings.toml +++ /dev/null @@ -1,55 +0,0 @@ -debug = true -debug_s = "true" -production = false -production_s = "false" - -code = 53 - -# errors -boolean_s_parse = "fals" - -arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -quarks = ["up", "down", "strange", "charm", "bottom", "top"] - -[diodes] -green = "off" - -[diodes.red] -brightness = 100 - -[diodes.blue] -blinking = [300, 700] - -[diodes.white.pattern] -name = "christmas" -inifinite = true - -[[items]] -name = "1" - -[[items]] -name = "2" - -[place] -number = 1 -name = "Torre di Pisa" -longitude = 43.7224985 -latitude = 10.3970522 -favorite = false -reviews = 3866 -rating = 4.5 - -[place.creator] -name = "John Smith" -username = "jsmith" -email = "jsmith@localhost" - -[proton] -up = 2 -down = 1 - -[divisors] -1 = 1 -2 = 2 -4 = 3 -5 = 2 diff --git a/tests/Settings.yaml b/tests/Settings.yaml deleted file mode 100644 index 8e79c29d..00000000 --- a/tests/Settings.yaml +++ /dev/null @@ -1,14 +0,0 @@ -debug: true -production: false -arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -place: - name: Torre di Pisa - longitude: 43.7224985 - latitude: 10.3970522 - favorite: false - reviews: 3866 - rating: 4.5 - creator: - name: John Smith - username: jsmith - email: jsmith@localhost diff --git a/tests/Settings2.default.ini b/tests/Settings2.default.ini deleted file mode 100644 index 16badd47..00000000 --- a/tests/Settings2.default.ini +++ /dev/null @@ -1,9 +0,0 @@ -debug = true -production = false -[place] -name = Torre di Pisa -longitude = 43.7224985 -latitude = 10.3970522 -favorite = false -reviews = 3866 -rating = 4.5 diff --git a/tests/async_builder.rs b/tests/async_builder.rs deleted file mode 100644 index bead9d38..00000000 --- a/tests/async_builder.rs +++ /dev/null @@ -1,142 +0,0 @@ -use async_trait::async_trait; -use config::{AsyncSource, Config, ConfigError, FileFormat, Format, Map, Value}; -use std::{env, fs, path, str::FromStr}; -use tokio::fs::read_to_string; - -#[derive(Debug)] -struct AsyncFile { - path: String, - format: FileFormat, -} - -/// This is a test only implementation to be used in tests -impl AsyncFile { - pub fn new(path: String, format: FileFormat) -> Self { - Self { path, format } - } -} - -#[async_trait] -impl AsyncSource for AsyncFile { - async fn collect(&self) -> Result, ConfigError> { - let mut path = env::current_dir().unwrap(); - let local = path::PathBuf::from_str(&self.path).unwrap(); - - path.extend(local.iter()); - let path = fs::canonicalize(path).map_err(|e| ConfigError::Foreign(Box::new(e)))?; - - let text = read_to_string(path) - .await - .map_err(|e| ConfigError::Foreign(Box::new(e)))?; - - self.format - .parse(Some(&self.path), &text) - .map_err(|e| ConfigError::Foreign(e)) - } -} - -#[tokio::test] -async fn test_single_async_file_source() { - let config = Config::builder() - .add_async_source(AsyncFile::new( - "tests/Settings.json".to_owned(), - FileFormat::Json, - )) - .build() - .await - .unwrap(); - - assert!(config.get::("debug").unwrap()); -} - -#[tokio::test] -async fn test_two_async_file_sources() { - let config = Config::builder() - .add_async_source(AsyncFile::new( - "tests/Settings.json".to_owned(), - FileFormat::Json, - )) - .add_async_source(AsyncFile::new( - "tests/Settings.toml".to_owned(), - FileFormat::Toml, - )) - .build() - .await - .unwrap(); - - assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); - assert!(config.get::("debug_json").unwrap()); - assert_eq!(1, config.get::("place.number").unwrap()); -} - -#[tokio::test] -async fn test_sync_to_async_file_sources() { - let config = Config::builder() - .add_source(config::File::new("tests/Settings", FileFormat::Json)) - .add_async_source(AsyncFile::new( - "tests/Settings.toml".to_owned(), - FileFormat::Toml, - )) - .build() - .await - .unwrap(); - - assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); - assert_eq!(1, config.get::("place.number").unwrap()); -} - -#[tokio::test] -async fn test_async_to_sync_file_sources() { - let config = Config::builder() - .add_async_source(AsyncFile::new( - "tests/Settings.toml".to_owned(), - FileFormat::Toml, - )) - .add_source(config::File::new("tests/Settings", FileFormat::Json)) - .build() - .await - .unwrap(); - - assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); - assert_eq!(1, config.get::("place.number").unwrap()); -} - -#[tokio::test] -async fn test_async_file_sources_with_defaults() { - let config = Config::builder() - .set_default("place.name", "Tower of London") - .unwrap() - .set_default("place.sky", "blue") - .unwrap() - .add_async_source(AsyncFile::new( - "tests/Settings.toml".to_owned(), - FileFormat::Toml, - )) - .build() - .await - .unwrap(); - - assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); - assert_eq!("blue", config.get::("place.sky").unwrap()); - assert_eq!(1, config.get::("place.number").unwrap()); -} - -#[tokio::test] -async fn test_async_file_sources_with_overrides() { - let config = Config::builder() - .set_override("place.name", "Tower of London") - .unwrap() - .add_async_source(AsyncFile::new( - "tests/Settings.toml".to_owned(), - FileFormat::Toml, - )) - .build() - .await - .unwrap(); - - assert_eq!( - "Tower of London", - config.get::("place.name").unwrap() - ); - assert_eq!(1, config.get::("place.number").unwrap()); -} diff --git a/tests/datetime.rs b/tests/datetime.rs deleted file mode 100644 index e15bdcec..00000000 --- a/tests/datetime.rs +++ /dev/null @@ -1,110 +0,0 @@ -#![cfg(all( - feature = "toml", - feature = "json", - feature = "yaml", - feature = "ini", - feature = "ron", -))] - -use chrono::{DateTime, TimeZone, Utc}; -use config::{Config, File, FileFormat}; - -fn make() -> Config { - Config::builder() - .add_source(File::from_str( - r#" - { - "json_datetime": "2017-05-10T02:14:53Z" - } - "#, - FileFormat::Json, - )) - .add_source(File::from_str( - r#" - yaml_datetime: 2017-06-12T10:58:30Z - "#, - FileFormat::Yaml, - )) - .add_source(File::from_str( - r#" - toml_datetime = 2017-05-11T14:55:15Z - "#, - FileFormat::Toml, - )) - .add_source(File::from_str( - r#" - ini_datetime = 2017-05-10T02:14:53Z - "#, - FileFormat::Ini, - )) - .add_source(File::from_str( - r#" - ( - ron_datetime: "2021-04-19T11:33:02Z" - ) - "#, - FileFormat::Ron, - )) - .build() - .unwrap() -} - -#[test] -fn test_datetime_string() { - let s = make(); - - // JSON - let date: String = s.get("json_datetime").unwrap(); - - assert_eq!(&date, "2017-05-10T02:14:53Z"); - - // TOML - let date: String = s.get("toml_datetime").unwrap(); - - assert_eq!(&date, "2017-05-11T14:55:15Z"); - - // YAML - let date: String = s.get("yaml_datetime").unwrap(); - - assert_eq!(&date, "2017-06-12T10:58:30Z"); - - // INI - let date: String = s.get("ini_datetime").unwrap(); - - assert_eq!(&date, "2017-05-10T02:14:53Z"); - - // RON - let date: String = s.get("ron_datetime").unwrap(); - - assert_eq!(&date, "2021-04-19T11:33:02Z"); -} - -#[test] -fn test_datetime() { - let s = make(); - - // JSON - let date: DateTime = s.get("json_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); - - // TOML - let date: DateTime = s.get("toml_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 11).and_hms(14, 55, 15)); - - // YAML - let date: DateTime = s.get("yaml_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 6, 12).and_hms(10, 58, 30)); - - // INI - let date: DateTime = s.get("ini_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); - - // RON - let date: DateTime = s.get("ron_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2021, 4, 19).and_hms(11, 33, 2)); -} diff --git a/tests/defaults.rs b/tests/defaults.rs deleted file mode 100644 index f7402596..00000000 --- a/tests/defaults.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -use config::Config; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(default)] -pub struct Settings { - pub db_host: String, -} - -impl Default for Settings { - fn default() -> Self { - Self { - db_host: String::from("default"), - } - } -} - -#[test] -fn set_defaults() { - let c = Config::default(); - let s: Settings = c.try_deserialize().expect("Deserialization failed"); - - assert_eq!(s.db_host, "default"); -} - -#[test] -fn try_from_defaults() { - let c = Config::try_from(&Settings::default()).expect("Serialization failed"); - let s: Settings = c.try_deserialize().expect("Deserialization failed"); - assert_eq!(s.db_host, "default"); -} diff --git a/tests/empty.rs b/tests/empty.rs deleted file mode 100644 index bce4f921..00000000 --- a/tests/empty.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; - -use config::Config; - -#[derive(Debug, Serialize, Deserialize)] -struct Settings { - #[serde(skip)] - foo: isize, - #[serde(skip)] - bar: u8, -} - -#[test] -fn empty_deserializes() { - let s: Settings = Config::default() - .try_deserialize() - .expect("Deserialization failed"); - assert_eq!(s.foo, 0); - assert_eq!(s.bar, 0); -} diff --git a/tests/env.rs b/tests/env.rs deleted file mode 100644 index ad252e41..00000000 --- a/tests/env.rs +++ /dev/null @@ -1,537 +0,0 @@ -use config::{Config, Environment, Source}; -use serde_derive::Deserialize; - -/// Reminder that tests using env variables need to use different env variable names, since -/// tests can be run in parallel - -#[test] -fn test_default() { - temp_env::with_var("A_B_C", Some("abc"), || { - let environment = Environment::default(); - - assert!(environment.collect().unwrap().contains_key("a_b_c")); - }) -} - -#[test] -fn test_prefix_is_removed_from_key() { - temp_env::with_var("B_A_C", Some("abc"), || { - let environment = Environment::with_prefix("B"); - - assert!(environment.collect().unwrap().contains_key("a_c")); - }) -} - -#[test] -fn test_prefix_with_variant_forms_of_spelling() { - temp_env::with_var("a_A_C", Some("abc"), || { - let environment = Environment::with_prefix("a"); - - assert!(environment.collect().unwrap().contains_key("a_c")); - }); - - temp_env::with_var("aB_A_C", Some("abc"), || { - let environment = Environment::with_prefix("aB"); - - assert!(environment.collect().unwrap().contains_key("a_c")); - }); - - temp_env::with_var("Ab_A_C", Some("abc"), || { - let environment = Environment::with_prefix("ab"); - - assert!(environment.collect().unwrap().contains_key("a_c")); - }); -} - -#[test] -fn test_separator_behavior() { - temp_env::with_var("C_B_A", Some("abc"), || { - let environment = Environment::with_prefix("C").separator("_"); - - assert!(environment.collect().unwrap().contains_key("b.a")); - }) -} - -#[test] -fn test_empty_value_is_ignored() { - temp_env::with_var("C_A_B", Some(""), || { - let environment = Environment::default().ignore_empty(true); - - assert!(!environment.collect().unwrap().contains_key("c_a_b")); - }) -} - -#[test] -fn test_keep_prefix() { - temp_env::with_var("C_A_B", Some(""), || { - // Do not keep the prefix - let environment = Environment::with_prefix("C"); - - assert!(environment.collect().unwrap().contains_key("a_b")); - - let environment = Environment::with_prefix("C").keep_prefix(false); - - assert!(environment.collect().unwrap().contains_key("a_b")); - - // Keep the prefix - let environment = Environment::with_prefix("C").keep_prefix(true); - - assert!(environment.collect().unwrap().contains_key("c_a_b")); - }) -} - -#[test] -fn test_custom_separator_behavior() { - temp_env::with_var("C.B.A", Some("abc"), || { - let environment = Environment::with_prefix("C").separator("."); - - assert!(environment.collect().unwrap().contains_key("b.a")); - }) -} - -#[test] -fn test_custom_prefix_separator_behavior() { - temp_env::with_var("C-B.A", Some("abc"), || { - let environment = Environment::with_prefix("C") - .separator(".") - .prefix_separator("-"); - - assert!(environment.collect().unwrap().contains_key("b.a")); - }) -} - -#[test] -fn test_parse_int() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - int_val: i32, - } - - temp_env::with_var("INT_VAL", Some("42"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Int") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestIntEnum = config.try_deserialize().unwrap(); - - assert!(matches!(config, TestIntEnum::Int(TestInt { int_val: 42 }))); - }) -} - -#[test] -fn test_parse_float() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - float_val: f64, - } - - temp_env::with_var("FLOAT_VAL", Some("42.3"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Float") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestFloatEnum = config.try_deserialize().unwrap(); - - // can't use `matches!` because of float value - match config { - TestFloatEnum::Float(TestFloat { float_val }) => assert_eq!(float_val, 42.3), - } - }) -} - -#[test] -fn test_parse_bool() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - bool_val: bool, - } - - temp_env::with_var("BOOL_VAL", Some("true"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Bool") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestBoolEnum = config.try_deserialize().unwrap(); - - assert!(matches!( - config, - TestBoolEnum::Bool(TestBool { bool_val: true }) - )); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"42\", expected i32")] -fn test_parse_off_int() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - #[allow(dead_code)] - int_val_1: i32, - } - - temp_env::with_var("INT_VAL_1", Some("42"), || { - let environment = Environment::default().try_parsing(false); - - let config = Config::builder() - .set_default("tag", "Int") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"42.3\", expected f64")] -fn test_parse_off_float() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - #[allow(dead_code)] - float_val_1: f64, - } - - temp_env::with_var("FLOAT_VAL_1", Some("42.3"), || { - let environment = Environment::default().try_parsing(false); - - let config = Config::builder() - .set_default("tag", "Float") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"true\", expected a boolean")] -fn test_parse_off_bool() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - #[allow(dead_code)] - bool_val_1: bool, - } - - temp_env::with_var("BOOL_VAL_1", Some("true"), || { - let environment = Environment::default().try_parsing(false); - - let config = Config::builder() - .set_default("tag", "Bool") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"not an int\", expected i32")] -fn test_parse_int_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - #[allow(dead_code)] - int_val_2: i32, - } - - temp_env::with_var("INT_VAL_2", Some("not an int"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Int") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"not a float\", expected f64")] -fn test_parse_float_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - #[allow(dead_code)] - float_val_2: f64, - } - - temp_env::with_var("FLOAT_VAL_2", Some("not a float"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Float") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -#[should_panic(expected = "invalid type: string \"not a bool\", expected a boolean")] -fn test_parse_bool_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - #[allow(dead_code)] - bool_val_2: bool, - } - - temp_env::with_var("BOOL_VAL_2", Some("not a bool"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "Bool") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - config.try_deserialize::().unwrap(); - }) -} - -#[test] -fn test_parse_string_and_list() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestStringEnum { - String(TestString), - } - - #[derive(Deserialize, Debug)] - struct TestString { - string_val: String, - string_list: Vec, - } - - temp_env::with_vars( - vec![ - ("LIST_STRING_LIST", Some("test,string")), - ("LIST_STRING_VAL", Some("test,string")), - ], - || { - let environment = Environment::default() - .prefix("LIST") - .list_separator(",") - .with_list_parse_key("string_list") - .try_parsing(true); - - let config = Config::builder() - .set_default("tag", "String") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestStringEnum = config.try_deserialize().unwrap(); - - match config { - TestStringEnum::String(TestString { - string_val, - string_list, - }) => { - assert_eq!(String::from("test,string"), string_val); - assert_eq!( - vec![String::from("test"), String::from("string")], - string_list - ); - } - } - }, - ) -} - -#[test] -fn test_parse_string() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestStringEnum { - String(TestString), - } - - #[derive(Deserialize, Debug)] - struct TestString { - string_val: String, - } - - temp_env::with_var("STRING_VAL", Some("test string"), || { - let environment = Environment::default().try_parsing(true); - - let config = Config::builder() - .set_default("tag", "String") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestStringEnum = config.try_deserialize().unwrap(); - - let test_string = String::from("test string"); - - match config { - TestStringEnum::String(TestString { string_val }) => { - assert_eq!(test_string, string_val) - } - } - }) -} - -#[test] -fn test_parse_string_list() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestListEnum { - StringList(TestList), - } - - #[derive(Deserialize, Debug)] - struct TestList { - string_list: Vec, - } - - temp_env::with_var("STRING_LIST", Some("test string"), || { - let environment = Environment::default().try_parsing(true).list_separator(" "); - - let config = Config::builder() - .set_default("tag", "StringList") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestListEnum = config.try_deserialize().unwrap(); - - let test_string = vec![String::from("test"), String::from("string")]; - - match config { - TestListEnum::StringList(TestList { string_list }) => { - assert_eq!(test_string, string_list) - } - } - }) -} - -#[test] -fn test_parse_off_string() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestStringEnum { - String(TestString), - } - - #[derive(Deserialize, Debug)] - struct TestString { - string_val_1: String, - } - - temp_env::with_var("STRING_VAL_1", Some("test string"), || { - let environment = Environment::default().try_parsing(false); - - let config = Config::builder() - .set_default("tag", "String") - .unwrap() - .add_source(environment) - .build() - .unwrap(); - - let config: TestStringEnum = config.try_deserialize().unwrap(); - - let test_string = String::from("test string"); - - match config { - TestStringEnum::String(TestString { string_val_1 }) => { - assert_eq!(test_string, string_val_1); - } - } - }) -} diff --git a/tests/errors.rs b/tests/errors.rs deleted file mode 100644 index 773fa469..00000000 --- a/tests/errors.rs +++ /dev/null @@ -1,155 +0,0 @@ -#![cfg(feature = "toml")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, ConfigError, File, FileFormat, Map, Value}; - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .build() - .unwrap() -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Toml)) - .build(); - - let path: PathBuf = ["tests", "Settings-invalid.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid TOML value, did you mean to use a quoted string? at line 2 column 9 in {}", - path.display() - ) - ); -} - -#[test] -fn test_error_type() { - let c = make(); - - let res = c.get::("boolean_s_parse"); - - let path: PathBuf = ["tests", "Settings.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid type: string \"fals\", expected a boolean for key `boolean_s_parse` in {}", - path.display() - ) - ); -} - -#[test] -fn test_error_type_detached() { - let c = make(); - - let value = c.get::("boolean_s_parse").unwrap(); - let res = value.try_deserialize::(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "invalid type: string \"fals\", expected a boolean".to_string() - ); -} - -#[test] -fn test_error_enum_de() { - #[derive(Debug, Deserialize, PartialEq)] - enum Diode { - Off, - Brightness(i32), - Blinking(i32, i32), - Pattern { name: String, inifinite: bool }, - } - - let on_v: Value = "on".into(); - let on_d = on_v.try_deserialize::(); - assert_eq!( - on_d.unwrap_err().to_string(), - "enum Diode does not have variant constructor on".to_string() - ); - - let array_v: Value = vec![100, 100].into(); - let array_d = array_v.try_deserialize::(); - assert_eq!( - array_d.unwrap_err().to_string(), - "value of enum Diode should be represented by either string or table with exactly one key" - ); - - let confused_v: Value = [ - ("Brightness".to_string(), 100.into()), - ("Blinking".to_string(), vec![300, 700].into()), - ] - .iter() - .cloned() - .collect::>() - .into(); - let confused_d = confused_v.try_deserialize::(); - assert_eq!( - confused_d.unwrap_err().to_string(), - "value of enum Diode should be represented by either string or table with exactly one key" - ); -} - -#[test] -fn error_with_path() { - #[derive(Debug, Deserialize)] - struct Inner { - #[allow(dead_code)] - test: i32, - } - - #[derive(Debug, Deserialize)] - struct Outer { - #[allow(dead_code)] - inner: Inner, - } - const CFG: &str = r#" -inner: - test: ABC -"#; - - let e = Config::builder() - .add_source(File::from_str(CFG, FileFormat::Yaml)) - .build() - .unwrap() - .try_deserialize::() - .unwrap_err(); - - if let ConfigError::Type { - key: Some(path), .. - } = e - { - assert_eq!(path, "inner.test"); - } else { - panic!("Wrong error {:?}", e); - } -} - -#[test] -fn test_error_root_not_table() { - match Config::builder() - .add_source(File::from_str(r#"false"#, FileFormat::Json5)) - .build() - { - Ok(_) => panic!("Should not merge if root is not a table"), - Err(e) => match e { - ConfigError::FileParse { cause, .. } => assert_eq!( - "invalid type: boolean `false`, expected a map", - format!("{}", cause) - ), - _ => panic!("Wrong error: {:?}", e), - }, - } -} diff --git a/tests/file.rs b/tests/file.rs deleted file mode 100644 index 9e4469ae..00000000 --- a/tests/file.rs +++ /dev/null @@ -1,70 +0,0 @@ -#![cfg(feature = "yaml")] - -use config::{Config, File, FileFormat}; - -#[test] -fn test_file_not_required() { - let res = Config::builder() - .add_source(File::new("tests/NoSettings", FileFormat::Yaml).required(false)) - .build(); - - assert!(res.is_ok()); -} - -#[test] -fn test_file_required_not_found() { - let res = Config::builder() - .add_source(File::new("tests/NoSettings", FileFormat::Yaml)) - .build(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration file \"tests/NoSettings\" not found".to_string() - ); -} - -#[test] -fn test_file_auto() { - let c = Config::builder() - .add_source(File::with_name("tests/Settings-production")) - .build() - .unwrap(); - - assert_eq!(c.get("debug").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some(true)); -} - -#[test] -fn test_file_auto_not_found() { - let res = Config::builder() - .add_source(File::with_name("tests/NoSettings")) - .build(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration file \"tests/NoSettings\" not found".to_string() - ); -} - -#[test] -fn test_file_ext() { - let c = Config::builder() - .add_source(File::with_name("tests/Settings.json")) - .build() - .unwrap(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} -#[test] -fn test_file_second_ext() { - let c = Config::builder() - .add_source(File::with_name("tests/Settings2.default")) - .build() - .unwrap(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} diff --git a/tests/file_ini.rs b/tests/file_ini.rs deleted file mode 100644 index 4ebf6e3c..00000000 --- a/tests/file_ini.rs +++ /dev/null @@ -1,68 +0,0 @@ -#![cfg(feature = "ini")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat}; - -#[derive(Debug, Deserialize, PartialEq)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - reviews: u64, - rating: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct Settings { - debug: f64, - place: Place, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Ini)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - assert_eq!( - s, - Settings { - debug: 1.0, - place: Place { - name: String::from("Torre di Pisa"), - longitude: 43.722_498_5, - latitude: 10.397_052_2, - favorite: false, - reviews: 3866, - rating: Some(4.5), - }, - } - ); -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Ini)) - .build(); - - let path: PathBuf = ["tests", "Settings-invalid.ini"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - r#"2:0 expecting "[Some('='), Some(':')]" but found EOF. in {}"#, - path.display() - ) - ); -} diff --git a/tests/file_json.rs b/tests/file_json.rs deleted file mode 100644 index e6609976..00000000 --- a/tests/file_json.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![cfg(feature = "json")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Json)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Json)) - .build(); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.json"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "expected `:` at line 4 column 1 in {}", - path_with_extension.display() - ) - ); -} - -#[test] -fn test_json_vec() { - let c = Config::builder() - .add_source(File::from_str( - r#" - { - "WASTE": ["example_dir1", "example_dir2"] - } - "#, - FileFormat::Json, - )) - .build() - .unwrap(); - - let v = c.get_array("WASTE").unwrap(); - let mut vi = v.into_iter(); - assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir1"); - assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir2"); - assert!(vi.next().is_none()); -} diff --git a/tests/file_json5.rs b/tests/file_json5.rs deleted file mode 100644 index 4bc17d71..00000000 --- a/tests/file_json5.rs +++ /dev/null @@ -1,91 +0,0 @@ -#![cfg(feature = "json5")] - -use serde_derive::Deserialize; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; -use std::path::PathBuf; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Json5)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Json5)) - .build(); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.json5"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - " --> 2:7\n |\n2 | ok: true␊\n | ^---\n |\n = expected null in {}", - path_with_extension.display() - ) - ); -} diff --git a/tests/file_ron.rs b/tests/file_ron.rs deleted file mode 100644 index 64e2caec..00000000 --- a/tests/file_ron.rs +++ /dev/null @@ -1,91 +0,0 @@ -#![cfg(feature = "ron")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - initials: (char, char), - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Ron)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.initials, ('T', 'P')); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Ron)) - .build(); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.ron"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!("4:1: Expected colon in {}", path_with_extension.display()) - ); -} diff --git a/tests/file_toml.rs b/tests/file_toml.rs deleted file mode 100644 index 9eed7887..00000000 --- a/tests/file_toml.rs +++ /dev/null @@ -1,103 +0,0 @@ -#![cfg(feature = "toml")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - number: PlaceNumber, - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct PlaceNumber(u8); - -#[derive(Debug, Deserialize, PartialEq)] -struct AsciiCode(i8); - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - code: AsciiCode, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -#[cfg(test)] -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.code, AsciiCode(53)); - assert_eq!(s.place.number, PlaceNumber(1)); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Toml)) - .build(); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid TOML value, did you mean to use a quoted string? at line 2 column 9 in {}", - path_with_extension.display() - ) - ); -} diff --git a/tests/file_yaml.rs b/tests/file_yaml.rs deleted file mode 100644 index 233b92cf..00000000 --- a/tests/file_yaml.rs +++ /dev/null @@ -1,93 +0,0 @@ -#![cfg(feature = "yaml")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Yaml)) - .build() - .unwrap() -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let res = Config::builder() - .add_source(File::new("tests/Settings-invalid", FileFormat::Yaml)) - .build(); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.yaml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "while parsing a block mapping, did not find expected key at \ - line 2 column 1 in {}", - path_with_extension.display() - ) - ); -} diff --git a/tests/get.rs b/tests/get.rs deleted file mode 100644 index fe66d34c..00000000 --- a/tests/get.rs +++ /dev/null @@ -1,280 +0,0 @@ -#![cfg(feature = "toml")] - -use serde_derive::Deserialize; - -use std::collections::HashSet; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, -} - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .build() - .unwrap() -} - -#[test] -fn test_not_found() { - let c = make(); - let res = c.get::("not_found"); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration property \"not_found\" not found".to_string() - ); -} - -#[test] -fn test_scalar() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} - -#[test] -fn test_scalar_type_loose() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("debug").ok(), Some("true".to_string())); - assert_eq!(c.get("debug").ok(), Some(1)); - assert_eq!(c.get("debug").ok(), Some(1.0)); - - assert_eq!(c.get("debug_s").ok(), Some(true)); - assert_eq!(c.get("debug_s").ok(), Some("true".to_string())); - assert_eq!(c.get("debug_s").ok(), Some(1)); - assert_eq!(c.get("debug_s").ok(), Some(1.0)); - - assert_eq!(c.get("production").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some("false".to_string())); - assert_eq!(c.get("production").ok(), Some(0)); - assert_eq!(c.get("production").ok(), Some(0.0)); - - assert_eq!(c.get("production_s").ok(), Some(false)); - assert_eq!(c.get("production_s").ok(), Some("false".to_string())); - assert_eq!(c.get("production_s").ok(), Some(0)); - assert_eq!(c.get("production_s").ok(), Some(0.0)); -} - -#[test] -fn test_get_scalar_path() { - let c = make(); - - assert_eq!(c.get("place.favorite").ok(), Some(false)); - assert_eq!( - c.get("place.creator.name").ok(), - Some("John Smith".to_string()) - ); -} - -#[test] -fn test_get_scalar_path_subscript() { - let c = make(); - - assert_eq!(c.get("arr[2]").ok(), Some(3)); - assert_eq!(c.get("items[0].name").ok(), Some("1".to_string())); - assert_eq!(c.get("items[1].name").ok(), Some("2".to_string())); - assert_eq!(c.get("items[-1].name").ok(), Some("2".to_string())); - assert_eq!(c.get("items[-2].name").ok(), Some("1".to_string())); -} - -#[test] -fn test_map() { - let c = make(); - let m: Map = c.get("place").unwrap(); - - assert_eq!(m.len(), 8); - assert_eq!( - m["name"].clone().into_string().unwrap(), - "Torre di Pisa".to_string() - ); - assert_eq!(m["reviews"].clone().into_int().unwrap(), 3866); -} - -#[test] -fn test_map_str() { - let c = make(); - let m: Map = c.get("place.creator").unwrap(); - - if cfg!(feature = "preserve_order") { - assert_eq!( - m.into_iter().collect::>(), - vec![ - ("name".to_string(), "John Smith".to_string()), - ("username".to_string(), "jsmith".to_string()), - ("email".to_string(), "jsmith@localhost".to_string()), - ] - ); - } else { - assert_eq!(m.len(), 3); - assert_eq!(m["name"], "John Smith".to_string()); - } -} - -#[test] -fn test_map_struct() { - #[derive(Debug, Deserialize)] - struct Settings { - place: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.place.len(), 8); - assert_eq!( - s.place["name"].clone().into_string().unwrap(), - "Torre di Pisa".to_string() - ); - assert_eq!(s.place["reviews"].clone().into_int().unwrap(), 3866); -} - -#[test] -fn test_file_struct() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); -} - -#[test] -fn test_scalar_struct() { - let c = make(); - - // Deserialize a scalar struct that has lots of different - // data types - let p: Place = c.get("place").unwrap(); - - assert_eq!(p.name, "Torre di Pisa"); - assert!(p.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(p.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!p.favorite); - assert_eq!(p.reviews, 3866); - assert_eq!(p.rating, Some(4.5)); - assert_eq!(p.telephone, None); -} - -#[test] -fn test_array_scalar() { - let c = make(); - let arr: Vec = c.get("arr").unwrap(); - - assert_eq!(arr.len(), 10); - assert_eq!(arr[3], 4); -} - -#[test] -fn test_struct_array() { - #[derive(Debug, Deserialize)] - struct Settings { - #[serde(rename = "arr")] - elements: Vec, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); -} - -#[test] -fn test_enum() { - #[derive(Debug, Deserialize, PartialEq)] - #[serde(rename_all = "lowercase")] - enum Diode { - Off, - Brightness(i32), - Blinking(i32, i32), - Pattern { name: String, inifinite: bool }, - } - #[derive(Debug, Deserialize)] - struct Settings { - diodes: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.diodes["green"], Diode::Off); - assert_eq!(s.diodes["red"], Diode::Brightness(100)); - assert_eq!(s.diodes["blue"], Diode::Blinking(300, 700)); - assert_eq!( - s.diodes["white"], - Diode::Pattern { - name: "christmas".into(), - inifinite: true, - } - ); -} - -#[test] -fn test_enum_key() { - #[derive(Debug, Deserialize, PartialEq, Eq, Hash)] - #[serde(rename_all = "lowercase")] - enum Quark { - Up, - Down, - Strange, - Charm, - Bottom, - Top, - } - - #[derive(Debug, Deserialize)] - struct Settings { - proton: Map, - // Just to make sure that set keys work too. - quarks: HashSet, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.proton[&Quark::Up], 2); - assert_eq!(s.quarks.len(), 6); -} - -#[test] -fn test_int_key() { - #[derive(Debug, Deserialize, PartialEq)] - struct Settings { - divisors: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - assert_eq!(s.divisors[&4], 3); - assert_eq!(s.divisors.len(), 4); -} diff --git a/tests/integer_range.rs b/tests/integer_range.rs deleted file mode 100644 index c3e88393..00000000 --- a/tests/integer_range.rs +++ /dev/null @@ -1,35 +0,0 @@ -use config::Config; - -#[test] -fn wrapping_u16() { - let c = Config::builder() - .add_source(config::File::from_str( - r#" - [settings] - port = 66000 - "#, - config::FileFormat::Toml, - )) - .build() - .unwrap(); - - let port: u16 = c.get("settings.port").unwrap(); - assert_eq!(port, 464); -} - -#[test] -fn nonwrapping_u32() { - let c = Config::builder() - .add_source(config::File::from_str( - r#" - [settings] - port = 66000 - "#, - config::FileFormat::Toml, - )) - .build() - .unwrap(); - - let port: u32 = c.get("settings.port").unwrap(); - assert_eq!(port, 66000); -} diff --git a/tests/legacy/datetime.rs b/tests/legacy/datetime.rs deleted file mode 100644 index 7e63e689..00000000 --- a/tests/legacy/datetime.rs +++ /dev/null @@ -1,134 +0,0 @@ -#![cfg(all( - feature = "toml", - feature = "json", - feature = "hjson", - feature = "yaml", - feature = "ini", - feature = "ron", -))] - -use self::chrono::{DateTime, TimeZone, Utc}; -use self::config::*; - -fn make() -> Config { - Config::default() - .merge(File::from_str( - r#" - { - "json_datetime": "2017-05-10T02:14:53Z" - } - "#, - FileFormat::Json, - )) - .unwrap() - .merge(File::from_str( - r#" - yaml_datetime: 2017-06-12T10:58:30Z - "#, - FileFormat::Yaml, - )) - .unwrap() - .merge(File::from_str( - r#" - toml_datetime = 2017-05-11T14:55:15Z - "#, - FileFormat::Toml, - )) - .unwrap() - .merge(File::from_str( - r#" - { - "hjson_datetime": "2017-05-10T02:14:53Z" - } - "#, - FileFormat::Hjson, - )) - .unwrap() - .merge(File::from_str( - r#" - ini_datetime = 2017-05-10T02:14:53Z - "#, - FileFormat::Ini, - )) - .unwrap() - .merge(File::from_str( - r#" - ( - ron_datetime: "2021-04-19T11:33:02Z" - ) - "#, - FileFormat::Ron, - )) - .unwrap() - .clone() -} - -#[test] -fn test_datetime_string() { - let s = make(); - - // JSON - let date: String = s.get("json_datetime").unwrap(); - - assert_eq!(&date, "2017-05-10T02:14:53Z"); - - // TOML - let date: String = s.get("toml_datetime").unwrap(); - - assert_eq!(&date, "2017-05-11T14:55:15Z"); - - // YAML - let date: String = s.get("yaml_datetime").unwrap(); - - assert_eq!(&date, "2017-06-12T10:58:30Z"); - - // HJSON - let date: String = s.get("hjson_datetime").unwrap(); - - assert_eq!(&date, "2017-05-10T02:14:53Z"); - - // INI - let date: String = s.get("ini_datetime").unwrap(); - - assert_eq!(&date, "2017-05-10T02:14:53Z"); - - // RON - let date: String = s.get("ron_datetime").unwrap(); - - assert_eq!(&date, "2021-04-19T11:33:02Z"); -} - -#[test] -fn test_datetime() { - let s = make(); - - // JSON - let date: DateTime = s.get("json_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); - - // TOML - let date: DateTime = s.get("toml_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 11).and_hms(14, 55, 15)); - - // YAML - let date: DateTime = s.get("yaml_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 6, 12).and_hms(10, 58, 30)); - - // HJSON - let date: DateTime = s.get("hjson_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); - - // INI - let date: DateTime = s.get("ini_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53)); - - // RON - let date: DateTime = s.get("ron_datetime").unwrap(); - - assert_eq!(date, Utc.ymd(2021, 4, 19).and_hms(11, 33, 2)); -} diff --git a/tests/legacy/env.rs b/tests/legacy/env.rs deleted file mode 100644 index cde14821..00000000 --- a/tests/legacy/env.rs +++ /dev/null @@ -1,348 +0,0 @@ - -use config::{Config}; -use serde_derive::Deserialize; -use std::env; - -/// Reminder that tests using env variables need to use different env variable names, since -/// tests can be run in parallel - - -#[test] -fn test_parse_int() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - int_val: i32, - } - - env::set_var("INT_VAL", "42"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); - - let config: TestIntEnum = config.try_deserialize().unwrap(); - - assert!(matches!(config, TestIntEnum::Int(TestInt { int_val: 42 }))); - - env::remove_var("INT_VAL"); -} - -#[test] -fn test_parse_float() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - float_val: f64, - } - - env::set_var("FLOAT_VAL", "42.3"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - - config.merge(environment).unwrap(); - - let config: TestFloatEnum = config.try_deserialize().unwrap(); - - // can't use `matches!` because of float value - match config { - TestFloatEnum::Float(TestFloat { float_val }) => assert_eq!(float_val, 42.3), - } - - env::remove_var("FLOAT_VAL"); -} - -#[test] -fn test_parse_bool() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - bool_val: bool, - } - - env::set_var("BOOL_VAL", "true"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - - config.merge(environment).unwrap(); - - let config: TestBoolEnum = config.try_deserialize().unwrap(); - - assert!(matches!( - config, - TestBoolEnum::Bool(TestBool { bool_val: true }), - )); - - env::remove_var("BOOL_VAL"); -} - -#[test] -#[should_panic(expected = "invalid type: string \"42\", expected i32")] -fn test_parse_off_int() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - int_val_1: i32, - } - - env::set_var("INT_VAL_1", "42"); - - let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("INT_VAL_1"); - - config.try_deserialize::().unwrap(); -} - -#[test] -#[should_panic(expected = "invalid type: string \"42.3\", expected f64")] -fn test_parse_off_float() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - float_val_1: f64, - } - - env::set_var("FLOAT_VAL_1", "42.3"); - - let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("FLOAT_VAL_1"); - - config.try_deserialize::().unwrap(); -} - -#[test] -#[should_panic(expected = "invalid type: string \"true\", expected a boolean")] -fn test_parse_off_bool() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - bool_val_1: bool, - } - - env::set_var("BOOL_VAL_1", "true"); - - let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("BOOL_VAL_1"); - - config.try_deserialize::().unwrap(); -} - -#[test] -#[should_panic(expected = "invalid type: string \"not an int\", expected i32")] -fn test_parse_int_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestIntEnum { - Int(TestInt), - } - - #[derive(Deserialize, Debug)] - struct TestInt { - int_val_2: i32, - } - - env::set_var("INT_VAL_2", "not an int"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("INT_VAL_2"); - - config.try_deserialize::().unwrap(); -} - -#[test] -#[should_panic(expected = "invalid type: string \"not a float\", expected f64")] -fn test_parse_float_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestFloatEnum { - Float(TestFloat), - } - - #[derive(Deserialize, Debug)] - struct TestFloat { - float_val_2: f64, - } - - env::set_var("FLOAT_VAL_2", "not a float"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("FLOAT_VAL_2"); - - config.try_deserialize::().unwrap(); -} - -#[test] -#[should_panic(expected = "invalid type: string \"not a bool\", expected a boolean")] -fn test_parse_bool_fail() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestBoolEnum { - Bool(TestBool), - } - - #[derive(Deserialize, Debug)] - struct TestBool { - bool_val_2: bool, - } - - env::set_var("BOOL_VAL_2", "not a bool"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - - config.merge(environment).unwrap(); - - env::remove_var("BOOL_VAL_2"); - - config.try_deserialize::().unwrap(); -} - -#[test] -fn test_parse_string() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestStringEnum { - String(TestString), - } - - #[derive(Deserialize, Debug)] - struct TestString { - string_val: String, - } - - env::set_var("STRING_VAL", "test string"); - - let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "String").unwrap(); - - config.merge(environment).unwrap(); - - let config: TestStringEnum = config.try_deserialize().unwrap(); - - let test_string = String::from("test string"); - - match config { - TestStringEnum::String(TestString { string_val }) => assert_eq!(test_string, string_val), - } - - env::remove_var("STRING_VAL"); -} - -#[test] -fn test_parse_off_string() { - // using a struct in an enum here to make serde use `deserialize_any` - #[derive(Deserialize, Debug)] - #[serde(tag = "tag")] - enum TestStringEnum { - String(TestString), - } - - #[derive(Deserialize, Debug)] - struct TestString { - string_val_1: String, - } - - env::set_var("STRING_VAL_1", "test string"); - - let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "String").unwrap(); - - config.merge(environment).unwrap(); - - let config: TestStringEnum = config.try_deserialize().unwrap(); - - let test_string = String::from("test string"); - - match config { - TestStringEnum::String(TestString { string_val_1 }) => { - assert_eq!(test_string, string_val_1) - } - } - - env::remove_var("STRING_VAL_1"); -} diff --git a/tests/legacy/errors.rs b/tests/legacy/errors.rs deleted file mode 100644 index 885580f3..00000000 --- a/tests/legacy/errors.rs +++ /dev/null @@ -1,134 +0,0 @@ -#![cfg(feature = "toml")] - -use std::path::PathBuf; - -use serde_derive::Deserialize; - -use config::{Config, ConfigError, File, FileFormat, Map, Value}; - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Toml)); - - let path: PathBuf = ["tests", "Settings-invalid.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid TOML value, did you mean to use a quoted string? at line 2 column 9 in {}", - path.display() - ) - ); -} - -#[test] -fn test_error_type() { - let c = make(); - - let res = c.get::("boolean_s_parse"); - - let path: PathBuf = ["tests", "Settings.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid type: string \"fals\", expected a boolean for key `boolean_s_parse` in {}", - path.display() - ) - ); -} - -#[test] -fn test_error_type_detached() { - let c = make(); - - let value = c.get::("boolean_s_parse").unwrap(); - let res = value.try_deserialize::(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "invalid type: string \"fals\", expected a boolean".to_string() - ); -} - -#[test] -fn test_error_enum_de() { - #[derive(Debug, Deserialize, PartialEq)] - enum Diode { - Off, - Brightness(i32), - Blinking(i32, i32), - Pattern { name: String, inifinite: bool }, - } - - let on_v: Value = "on".into(); - let on_d = on_v.try_deserialize::(); - assert_eq!( - on_d.unwrap_err().to_string(), - "enum Diode does not have variant constructor on".to_string() - ); - - let array_v: Value = vec![100, 100].into(); - let array_d = array_v.try_deserialize::(); - assert_eq!( - array_d.unwrap_err().to_string(), - "value of enum Diode should be represented by either string or table with exactly one key" - ); - - let confused_v: Value = [ - ("Brightness".to_string(), 100.into()), - ("Blinking".to_string(), vec![300, 700].into()), - ] - .iter() - .cloned() - .collect::>() - .into(); - let confused_d = confused_v.try_deserialize::(); - assert_eq!( - confused_d.unwrap_err().to_string(), - "value of enum Diode should be represented by either string or table with exactly one key" - ); -} - -#[test] -fn error_with_path() { - #[derive(Debug, Deserialize)] - struct Inner { - #[allow(dead_code)] - test: i32, - } - - #[derive(Debug, Deserialize)] - struct Outer { - #[allow(dead_code)] - inner: Inner, - } - const CFG: &str = r#" -inner: - test: ABC -"#; - - let mut cfg = Config::default(); - cfg.merge(File::from_str(CFG, FileFormat::Yaml)).unwrap(); - let e = cfg.try_deserialize::().unwrap_err(); - if let ConfigError::Type { - key: Some(path), .. - } = e - { - assert_eq!(path, "inner.test"); - } else { - panic!("Wrong error {:?}", e); - } -} diff --git a/tests/legacy/file.rs b/tests/legacy/file.rs deleted file mode 100644 index 59006465..00000000 --- a/tests/legacy/file.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![cfg(feature = "yaml")] - -use config::{Config, File, FileFormat}; - -#[test] -fn test_file_not_required() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/NoSettings", FileFormat::Yaml).required(false)); - - assert!(res.is_ok()); -} - -#[test] -fn test_file_required_not_found() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/NoSettings", FileFormat::Yaml)); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration file \"tests/NoSettings\" not found".to_string() - ); -} - -#[test] -fn test_file_auto() { - let mut c = Config::default(); - c.merge(File::with_name("tests/Settings-production")) - .unwrap(); - - assert_eq!(c.get("debug").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some(true)); -} - -#[test] -fn test_file_auto_not_found() { - let mut c = Config::default(); - let res = c.merge(File::with_name("tests/NoSettings")); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration file \"tests/NoSettings\" not found".to_string() - ); -} - -#[test] -fn test_file_ext() { - let mut c = Config::default(); - c.merge(File::with_name("tests/Settings.json")).unwrap(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} diff --git a/tests/legacy/file_ini.rs b/tests/legacy/file_ini.rs deleted file mode 100644 index 5cbf63d4..00000000 --- a/tests/legacy/file_ini.rs +++ /dev/null @@ -1,66 +0,0 @@ -#![cfg(feature = "ini")] - -use serde_derive::Deserialize; -use std::path::PathBuf; - -use config::{Config, File, FileFormat}; - -#[derive(Debug, Deserialize, PartialEq)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - reviews: u64, - rating: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct Settings { - debug: f64, - place: Place, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Ini)) - .unwrap(); - c -} - -#[test] -fn test_file() { - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - assert_eq!( - s, - Settings { - debug: 1.0, - place: Place { - name: String::from("Torre di Pisa"), - longitude: 43.722_498_5, - latitude: 10.397_052_2, - favorite: false, - reviews: 3866, - rating: Some(4.5), - }, - } - ); -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Ini)); - - let path: PathBuf = ["tests", "Settings-invalid.ini"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - r#"2:0 expecting "[Some('='), Some(':')]" but found EOF. in {}"#, - path.display() - ) - ); -} diff --git a/tests/legacy/file_json.rs b/tests/legacy/file_json.rs deleted file mode 100644 index 72fd5ebb..00000000 --- a/tests/legacy/file_json.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![cfg(feature = "json")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Json)) - .unwrap(); - - c -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Json)); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.json"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "expected `:` at line 4 column 1 in {}", - path_with_extension.display() - ) - ); -} - -#[test] -fn test_json_vec() { - let c = Config::default() - .merge(File::from_str( - r#" - { - "WASTE": ["example_dir1", "example_dir2"] - } - "#, - FileFormat::Json, - )) - .unwrap() - .clone(); - - let v = c.get_array("WASTE").unwrap(); - let mut vi = v.into_iter(); - assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir1"); - assert_eq!(vi.next().unwrap().into_string().unwrap(), "example_dir2"); - assert!(vi.next().is_none()); -} diff --git a/tests/legacy/file_ron.rs b/tests/legacy/file_ron.rs deleted file mode 100644 index 7f31c0e9..00000000 --- a/tests/legacy/file_ron.rs +++ /dev/null @@ -1,90 +0,0 @@ -#![cfg(feature = "ron")] - -use serde_derive::Deserialize; -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - initials: (char, char), - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Ron)) - .unwrap(); - - c -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.initials, ('T', 'P')); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Ron)); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.ron"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!("4:1: Expected colon in {}", path_with_extension.display()) - ); -} diff --git a/tests/legacy/file_toml.rs b/tests/legacy/file_toml.rs deleted file mode 100644 index 795f1ab6..00000000 --- a/tests/legacy/file_toml.rs +++ /dev/null @@ -1,102 +0,0 @@ -#![cfg(feature = "toml")] - -use serde_derive::Deserialize; -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - number: PlaceNumber, - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize, PartialEq)] -struct PlaceNumber(u8); - -#[derive(Debug, Deserialize, PartialEq)] -struct AsciiCode(i8); - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - code: AsciiCode, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -#[cfg(test)] -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.code, AsciiCode(53)); - assert_eq!(s.place.number, PlaceNumber(1)); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Toml)); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.toml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "invalid TOML value, did you mean to use a quoted string? at line 2 column 9 in {}", - path_with_extension.display() - ) - ); -} diff --git a/tests/legacy/file_yaml.rs b/tests/legacy/file_yaml.rs deleted file mode 100644 index 21d41384..00000000 --- a/tests/legacy/file_yaml.rs +++ /dev/null @@ -1,93 +0,0 @@ -#![cfg(feature = "yaml")] - -use serde_derive::Deserialize; - -use std::path::PathBuf; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - creator: Map, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, - #[serde(rename = "arr")] - elements: Vec, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Yaml)) - .unwrap(); - - c -} - -#[test] -fn test_file() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); - if cfg!(feature = "preserve_order") { - assert_eq!( - s.place - .creator - .into_iter() - .collect::>(), - vec![ - ("name".to_string(), "John Smith".into()), - ("username".into(), "jsmith".into()), - ("email".into(), "jsmith@localhost".into()), - ] - ); - } else { - assert_eq!( - s.place.creator["name"].clone().into_string().unwrap(), - "John Smith".to_string() - ); - } -} - -#[test] -fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Yaml)); - - let path_with_extension: PathBuf = ["tests", "Settings-invalid.yaml"].iter().collect(); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - format!( - "while parsing a block mapping, did not find expected key at \ - line 2 column 1 in {}", - path_with_extension.display() - ) - ); -} diff --git a/tests/legacy/get.rs b/tests/legacy/get.rs deleted file mode 100644 index 24f0d352..00000000 --- a/tests/legacy/get.rs +++ /dev/null @@ -1,280 +0,0 @@ -#![cfg(feature = "toml")] - -use serde_derive::Deserialize; -use std::collections::HashSet; - -use config::{Config, File, FileFormat, Map, Value}; -use float_cmp::ApproxEqUlps; - -#[derive(Debug, Deserialize)] -struct Place { - name: String, - longitude: f64, - latitude: f64, - favorite: bool, - telephone: Option, - reviews: u64, - rating: Option, -} - -#[derive(Debug, Deserialize)] -struct Settings { - debug: f64, - production: Option, - place: Place, -} - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_not_found() { - let c = make(); - let res = c.get::("not_found"); - - assert!(res.is_err()); - assert_eq!( - res.unwrap_err().to_string(), - "configuration property \"not_found\" not found".to_string() - ); -} - -#[test] -fn test_scalar() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("production").ok(), Some(false)); -} - -#[test] -fn test_scalar_type_loose() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("debug").ok(), Some("true".to_string())); - assert_eq!(c.get("debug").ok(), Some(1)); - assert_eq!(c.get("debug").ok(), Some(1.0)); - - assert_eq!(c.get("debug_s").ok(), Some(true)); - assert_eq!(c.get("debug_s").ok(), Some("true".to_string())); - assert_eq!(c.get("debug_s").ok(), Some(1)); - assert_eq!(c.get("debug_s").ok(), Some(1.0)); - - assert_eq!(c.get("production").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some("false".to_string())); - assert_eq!(c.get("production").ok(), Some(0)); - assert_eq!(c.get("production").ok(), Some(0.0)); - - assert_eq!(c.get("production_s").ok(), Some(false)); - assert_eq!(c.get("production_s").ok(), Some("false".to_string())); - assert_eq!(c.get("production_s").ok(), Some(0)); - assert_eq!(c.get("production_s").ok(), Some(0.0)); -} - -#[test] -fn test_get_scalar_path() { - let c = make(); - - assert_eq!(c.get("place.favorite").ok(), Some(false)); - assert_eq!( - c.get("place.creator.name").ok(), - Some("John Smith".to_string()) - ); -} - -#[test] -fn test_get_scalar_path_subscript() { - let c = make(); - - assert_eq!(c.get("arr[2]").ok(), Some(3)); - assert_eq!(c.get("items[0].name").ok(), Some("1".to_string())); - assert_eq!(c.get("items[1].name").ok(), Some("2".to_string())); - assert_eq!(c.get("items[-1].name").ok(), Some("2".to_string())); - assert_eq!(c.get("items[-2].name").ok(), Some("1".to_string())); -} - -#[test] -fn test_map() { - let c = make(); - let m: Map = c.get("place").unwrap(); - - assert_eq!(m.len(), 8); - assert_eq!( - m["name"].clone().into_string().unwrap(), - "Torre di Pisa".to_string() - ); - assert_eq!(m["reviews"].clone().into_int().unwrap(), 3866); -} - -#[test] -fn test_map_str() { - let c = make(); - let m: Map = c.get("place.creator").unwrap(); - - if cfg!(feature = "preserve_order") { - assert_eq!( - m.into_iter().collect::>(), - vec![ - ("name".to_string(), "John Smith".to_string()), - ("username".to_string(), "jsmith".to_string()), - ("email".to_string(), "jsmith@localhost".to_string()), - ] - ); - } else { - assert_eq!(m.len(), 3); - assert_eq!(m["name"], "John Smith".to_string()); - } -} - -#[test] -fn test_map_struct() { - #[derive(Debug, Deserialize)] - struct Settings { - place: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.place.len(), 8); - assert_eq!( - s.place["name"].clone().into_string().unwrap(), - "Torre di Pisa".to_string() - ); - assert_eq!(s.place["reviews"].clone().into_int().unwrap(), 3866); -} - -#[test] -fn test_file_struct() { - let c = make(); - - // Deserialize the entire file as single struct - let s: Settings = c.try_deserialize().unwrap(); - - assert!(s.debug.approx_eq_ulps(&1.0, 2)); - assert_eq!(s.production, Some("false".to_string())); - assert_eq!(s.place.name, "Torre di Pisa"); - assert!(s.place.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(s.place.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!s.place.favorite); - assert_eq!(s.place.reviews, 3866); - assert_eq!(s.place.rating, Some(4.5)); - assert_eq!(s.place.telephone, None); -} - -#[test] -fn test_scalar_struct() { - let c = make(); - - // Deserialize a scalar struct that has lots of different - // data types - let p: Place = c.get("place").unwrap(); - - assert_eq!(p.name, "Torre di Pisa"); - assert!(p.longitude.approx_eq_ulps(&43.722_498_5, 2)); - assert!(p.latitude.approx_eq_ulps(&10.397_052_2, 2)); - assert!(!p.favorite); - assert_eq!(p.reviews, 3866); - assert_eq!(p.rating, Some(4.5)); - assert_eq!(p.telephone, None); -} - -#[test] -fn test_array_scalar() { - let c = make(); - let arr: Vec = c.get("arr").unwrap(); - - assert_eq!(arr.len(), 10); - assert_eq!(arr[3], 4); -} - -#[test] -fn test_struct_array() { - #[derive(Debug, Deserialize)] - struct Settings { - #[serde(rename = "arr")] - elements: Vec, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.elements.len(), 10); - assert_eq!(s.elements[3], "4".to_string()); -} - -#[test] -fn test_enum() { - #[derive(Debug, Deserialize, PartialEq)] - #[serde(rename_all = "lowercase")] - enum Diode { - Off, - Brightness(i32), - Blinking(i32, i32), - Pattern { name: String, inifinite: bool }, - } - #[derive(Debug, Deserialize)] - struct Settings { - diodes: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.diodes["green"], Diode::Off); - assert_eq!(s.diodes["red"], Diode::Brightness(100)); - assert_eq!(s.diodes["blue"], Diode::Blinking(300, 700)); - assert_eq!( - s.diodes["white"], - Diode::Pattern { - name: "christmas".into(), - inifinite: true, - } - ); -} - -#[test] -fn test_enum_key() { - #[derive(Debug, Deserialize, PartialEq, Eq, Hash)] - #[serde(rename_all = "lowercase")] - enum Quark { - Up, - Down, - Strange, - Charm, - Bottom, - Top, - } - - #[derive(Debug, Deserialize)] - struct Settings { - proton: Map, - // Just to make sure that set keys work too. - quarks: HashSet, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - - assert_eq!(s.proton[&Quark::Up], 2); - assert_eq!(s.quarks.len(), 6); -} - -#[test] -fn test_int_key() { - #[derive(Debug, Deserialize, PartialEq)] - struct Settings { - divisors: Map, - } - - let c = make(); - let s: Settings = c.try_deserialize().unwrap(); - assert_eq!(s.divisors[&4], 3); - assert_eq!(s.divisors.len(), 4); -} diff --git a/tests/legacy/merge.rs b/tests/legacy/merge.rs deleted file mode 100644 index 74463d66..00000000 --- a/tests/legacy/merge.rs +++ /dev/null @@ -1,60 +0,0 @@ -#![cfg(feature = "toml")] - -use config::{Config, File, FileFormat, Map}; - -fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c.merge(File::new("tests/Settings-production", FileFormat::Toml)) - .unwrap(); - - c -} - -#[test] -fn test_merge() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some(true)); - assert_eq!(c.get("place.rating").ok(), Some(4.9)); - - if cfg!(feature = "preserve_order") { - let m: Map = c.get("place.creator").unwrap(); - assert_eq!( - m.into_iter().collect::>(), - vec![ - ("name".to_string(), "Somebody New".to_string()), - ("username".to_string(), "jsmith".to_string()), - ("email".to_string(), "jsmith@localhost".to_string()), - ] - ); - } else { - assert_eq!( - c.get("place.creator.name").ok(), - Some("Somebody New".to_string()) - ); - } -} - -#[test] -fn test_merge_whole_config() { - let mut c1 = Config::default(); - let mut c2 = Config::default(); - - c1.set("x", 10).unwrap(); - c2.set("y", 25).unwrap(); - - assert_eq!(c1.get("x").ok(), Some(10)); - assert_eq!(c2.get::<()>("x").ok(), None); - - assert_eq!(c2.get("y").ok(), Some(25)); - assert_eq!(c1.get::<()>("y").ok(), None); - - c1.merge(c2).unwrap(); - - assert_eq!(c1.get("x").ok(), Some(10)); - assert_eq!(c1.get("y").ok(), Some(25)); -} diff --git a/tests/legacy/mod.rs b/tests/legacy/mod.rs deleted file mode 100644 index cd0a16a5..00000000 --- a/tests/legacy/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod datetime; -pub mod errors; -pub mod file; -pub mod file_ini; -pub mod file_json; -pub mod file_ron; -pub mod file_toml; -pub mod file_yaml; -pub mod get; -pub mod merge; -pub mod set; diff --git a/tests/legacy/set.rs b/tests/legacy/set.rs deleted file mode 100644 index 169e462a..00000000 --- a/tests/legacy/set.rs +++ /dev/null @@ -1,91 +0,0 @@ -use config::{Config, File, FileFormat}; - -#[test] -fn test_set_scalar() { - let mut c = Config::default(); - - c.set("value", true).unwrap(); - - assert_eq!(c.get("value").ok(), Some(true)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_scalar_default() { - let mut c = Config::default(); - - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c.set_default("debug", false).unwrap(); - c.set_default("staging", false).unwrap(); - - assert_eq!(c.get("debug").ok(), Some(true)); - assert_eq!(c.get("staging").ok(), Some(false)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_scalar_path() { - let mut c = Config::default(); - - c.set("first.second.third", true).unwrap(); - - assert_eq!(c.get("first.second.third").ok(), Some(true)); - - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c.set_default("place.favorite", true).unwrap(); - c.set_default("place.blocked", true).unwrap(); - - assert_eq!(c.get("place.favorite").ok(), Some(false)); - assert_eq!(c.get("place.blocked").ok(), Some(true)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_arr_path() { - let mut c = Config::default(); - - c.set("items[0].name", "Ivan").unwrap(); - - assert_eq!(c.get("items[0].name").ok(), Some("Ivan".to_string())); - - c.set("data[0].things[1].name", "foo").unwrap(); - c.set("data[0].things[1].value", 42).unwrap(); - c.set("data[1]", 0).unwrap(); - - assert_eq!( - c.get("data[0].things[1].name").ok(), - Some("foo".to_string()) - ); - assert_eq!(c.get("data[0].things[1].value").ok(), Some(42)); - assert_eq!(c.get("data[1]").ok(), Some(0)); - - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c.set("items[0].name", "John").unwrap(); - - assert_eq!(c.get("items[0].name").ok(), Some("John".to_string())); - - c.set("items[2]", "George").unwrap(); - - assert_eq!(c.get("items[2]").ok(), Some("George".to_string())); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_capital() { - let mut c = Config::default(); - - c.set_default("this", false).unwrap(); - c.set("ThAt", true).unwrap(); - c.merge(File::from_str("{\"logLevel\": 5}", FileFormat::Json)) - .unwrap(); - - assert_eq!(c.get("this").ok(), Some(false)); - assert_eq!(c.get("ThAt").ok(), Some(true)); - assert_eq!(c.get("logLevel").ok(), Some(5)); -} diff --git a/tests/legacy_tests.rs b/tests/legacy_tests.rs deleted file mode 100644 index ce288387..00000000 --- a/tests/legacy_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[allow(deprecated)] -pub mod legacy; diff --git a/tests/merge.rs b/tests/merge.rs deleted file mode 100644 index 0469064f..00000000 --- a/tests/merge.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![cfg(feature = "toml")] - -use config::{Config, File, FileFormat, Map}; - -fn make() -> Config { - Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .add_source(File::new("tests/Settings-production", FileFormat::Toml)) - .build() - .unwrap() -} - -#[test] -fn test_merge() { - let c = make(); - - assert_eq!(c.get("debug").ok(), Some(false)); - assert_eq!(c.get("production").ok(), Some(true)); - assert_eq!(c.get("place.rating").ok(), Some(4.9)); - - if cfg!(feature = "preserve_order") { - let m: Map = c.get("place.creator").unwrap(); - assert_eq!( - m.into_iter().collect::>(), - vec![ - ("name".to_string(), "Somebody New".to_string()), - ("username".to_string(), "jsmith".to_string()), - ("email".to_string(), "jsmith@localhost".to_string()), - ] - ); - } else { - assert_eq!( - c.get("place.creator.name").ok(), - Some("Somebody New".to_string()) - ); - } -} - -#[test] -fn test_merge_whole_config() { - let builder1 = Config::builder().set_override("x", 10).unwrap(); - let builder2 = Config::builder().set_override("y", 25).unwrap(); - - let config1 = builder1.build_cloned().unwrap(); - let config2 = builder2.build_cloned().unwrap(); - - assert_eq!(config1.get("x").ok(), Some(10)); - assert_eq!(config2.get::<()>("x").ok(), None); - - assert_eq!(config2.get("y").ok(), Some(25)); - assert_eq!(config1.get::<()>("y").ok(), None); - - let config3 = builder1.add_source(config2).build().unwrap(); - - assert_eq!(config3.get("x").ok(), Some(10)); - assert_eq!(config3.get("y").ok(), Some(25)); -} diff --git a/tests/set.rs b/tests/set.rs deleted file mode 100644 index 59b5b849..00000000 --- a/tests/set.rs +++ /dev/null @@ -1,91 +0,0 @@ -use config::{Config, File, FileFormat}; - -#[test] -fn test_set_override_scalar() { - let config = Config::builder() - .set_override("value", true) - .and_then(|b| b.build()) - .unwrap(); - - assert_eq!(config.get("value").ok(), Some(true)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_scalar_default() { - let config = Config::builder() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .set_default("debug", false) - .unwrap() - .set_default("staging", false) - .unwrap() - .build() - .unwrap(); - - assert_eq!(config.get("debug").ok(), Some(true)); - assert_eq!(config.get("staging").ok(), Some(false)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_scalar_path() { - let config = Config::builder() - .set_override("first.second.third", true) - .unwrap() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .set_default("place.favorite", true) - .unwrap() - .set_default("place.blocked", true) - .unwrap() - .build() - .unwrap(); - - assert_eq!(config.get("first.second.third").ok(), Some(true)); - assert_eq!(config.get("place.favorite").ok(), Some(false)); - assert_eq!(config.get("place.blocked").ok(), Some(true)); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_arr_path() { - let config = Config::builder() - .set_override("items[0].name", "Ivan") - .unwrap() - .set_override("data[0].things[1].name", "foo") - .unwrap() - .set_override("data[0].things[1].value", 42) - .unwrap() - .set_override("data[1]", 0) - .unwrap() - .add_source(File::new("tests/Settings", FileFormat::Toml)) - .set_override("items[2]", "George") - .unwrap() - .build() - .unwrap(); - - assert_eq!(config.get("items[0].name").ok(), Some("Ivan".to_string())); - assert_eq!( - config.get("data[0].things[1].name").ok(), - Some("foo".to_string()) - ); - assert_eq!(config.get("data[0].things[1].value").ok(), Some(42)); - assert_eq!(config.get("data[1]").ok(), Some(0)); - assert_eq!(config.get("items[2]").ok(), Some("George".to_string())); -} - -#[cfg(feature = "toml")] -#[test] -fn test_set_capital() { - let config = Config::builder() - .set_default("this", false) - .unwrap() - .set_override("ThAt", true) - .unwrap() - .add_source(File::from_str("{\"logLevel\": 5}", FileFormat::Json)) - .build() - .unwrap(); - - assert_eq!(config.get("this").ok(), Some(false)); - assert_eq!(config.get("ThAt").ok(), Some(true)); - assert_eq!(config.get("logLevel").ok(), Some(5)); -} diff --git a/tests/types/i64.toml b/tests/types/i64.toml deleted file mode 100644 index 4ad173a1..00000000 --- a/tests/types/i64.toml +++ /dev/null @@ -1 +0,0 @@ -value = 120 diff --git a/tests/weird_keys.rs b/tests/weird_keys.rs deleted file mode 100644 index c997fe05..00000000 --- a/tests/weird_keys.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Please note: This file is named "weird" keys because these things are normally not keys, not -// because your software is weird if it expects these keys in the config file. -// -// Please don't be offended! -// - -use serde_derive::{Deserialize, Serialize}; - -use config::{File, FileFormat}; - -/// Helper fn to test the different deserializations -fn test_config_as<'a, T>(config: &str, format: FileFormat) -> T -where - T: serde::Deserialize<'a> + std::fmt::Debug, -{ - let cfg = config::Config::builder() - .add_source(File::from_str(config, format)) - .build(); - - assert!(cfg.is_ok(), "Config could not be built: {:?}", cfg); - let cfg = cfg.unwrap().try_deserialize(); - - assert!(cfg.is_ok(), "Config could not be transformed: {:?}", cfg); - let cfg: T = cfg.unwrap(); - cfg -} - -#[derive(Debug, Serialize, Deserialize)] -struct SettingsColon { - #[serde(rename = "foo:foo")] - foo: u8, - - bar: u8, -} - -#[test] -fn test_colon_key_toml() { - let config = r#" - "foo:foo" = 8 - bar = 12 - "#; - - let cfg = test_config_as::(config, FileFormat::Toml); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -} - -#[test] -fn test_colon_key_json() { - let config = r#" {"foo:foo": 8, "bar": 12 } "#; - - let cfg = test_config_as::(config, FileFormat::Json); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -} - -#[derive(Debug, Serialize, Deserialize)] -struct SettingsSlash { - #[serde(rename = "foo/foo")] - foo: u8, - bar: u8, -} - -#[test] -fn test_slash_key_toml() { - let config = r#" - "foo/foo" = 8 - bar = 12 - "#; - - let cfg = test_config_as::(config, FileFormat::Toml); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -} - -#[test] -fn test_slash_key_json() { - let config = r#" {"foo/foo": 8, "bar": 12 } "#; - - let cfg = test_config_as::(config, FileFormat::Json); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -} - -#[derive(Debug, Serialize, Deserialize)] -struct SettingsDoubleBackslash { - #[serde(rename = "foo\\foo")] - foo: u8, - bar: u8, -} - -#[test] -fn test_doublebackslash_key_toml() { - let config = r#" - "foo\\foo" = 8 - bar = 12 - "#; - - let cfg = test_config_as::(config, FileFormat::Toml); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -} - -#[test] -fn test_doublebackslash_key_json() { - let config = r#" {"foo\\foo": 8, "bar": 12 } "#; - - let cfg = test_config_as::(config, FileFormat::Json); - assert_eq!(cfg.foo, 8); - assert_eq!(cfg.bar, 12); -}