aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeongik Cha <jeongik@google.com>2023-09-27 10:19:08 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-09-27 10:19:08 +0000
commit576d980bd425fb34497c8908392d54167ee21841 (patch)
tree1d8ee82a8ceb5276c1ae961bdb7c5b710966f685
parent919c1e4e74e1a5907ba6e6d75e4af0821df1e725 (diff)
parent886b4178e770cc971d1995b5bf62fb941d640bdd (diff)
downloadconfig-576d980bd425fb34497c8908392d54167ee21841.tar.gz
Import config am: 1bac1e0979 am: b663f4090b am: 886b4178e7
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/config/+/2752271 Change-Id: Id7ba0a86a31a8faa03318440ea8dbb933c16e82b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--.clippy.toml1
-rw-r--r--.editorconfig7
-rw-r--r--.github/dependabot.yml10
-rw-r--r--.github/workflows/fixupmerge.yml12
-rw-r--r--.github/workflows/msrv.yml160
-rw-r--r--.gitignore2
-rw-r--r--Android.bp30
-rw-r--r--CHANGELOG.md283
-rw-r--r--Cargo.toml141
-rw-r--r--Cargo.toml.orig53
-rw-r--r--LICENSE230
-rw-r--r--LICENSE-APACHE201
-rw-r--r--LICENSE-MIT25
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--MODULE_LICENSE_MIT0
-rw-r--r--OWNERS1
-rw-r--r--README.md66
-rw-r--r--examples/async_source/main.rs74
-rw-r--r--examples/custom_format/main.rs50
-rw-r--r--examples/env-list/main.rs25
-rw-r--r--examples/glob/conf/00-default.toml1
-rw-r--r--examples/glob/conf/05-some.yml2
-rw-r--r--examples/glob/conf/99-extra.json5
-rw-r--r--examples/glob/main.rs66
-rw-r--r--examples/global/main.rs23
-rw-r--r--examples/hierarchical-env/config/default.toml17
-rw-r--r--examples/hierarchical-env/config/development.toml4
-rw-r--r--examples/hierarchical-env/config/production.toml13
-rw-r--r--examples/hierarchical-env/main.rs10
-rw-r--r--examples/hierarchical-env/settings.rs76
-rw-r--r--examples/simple/Settings.toml3
-rw-r--r--examples/simple/main.rs21
-rw-r--r--examples/watch/Settings.toml3
-rw-r--r--examples/watch/main.rs70
-rw-r--r--src/builder.rs370
-rw-r--r--src/config.rs218
-rw-r--r--src/de.rs468
-rw-r--r--src/env.rs244
-rw-r--r--src/error.rs244
-rw-r--r--src/file/format/ini.rs37
-rw-r--r--src/file/format/json.rs58
-rw-r--r--src/file/format/json5.rs66
-rw-r--r--src/file/format/mod.rs147
-rw-r--r--src/file/format/ron.rs66
-rw-r--r--src/file/format/toml.rs49
-rw-r--r--src/file/format/yaml.rs108
-rw-r--r--src/file/mod.rs149
-rw-r--r--src/file/source/file.rs141
-rw-r--r--src/file/source/mod.rs38
-rw-r--r--src/file/source/string.rs33
-rw-r--r--src/format.rs23
-rw-r--r--src/lib.rs44
-rw-r--r--src/map.rs4
-rw-r--r--src/path/mod.rs250
-rw-r--r--src/path/parser.rs131
-rw-r--r--src/ser.rs720
-rw-r--r--src/source.rs145
-rw-r--r--src/value.rs883
-rw-r--r--tests/Settings-invalid.hjson4
-rw-r--r--tests/Settings-invalid.ini2
-rw-r--r--tests/Settings-invalid.json4
-rw-r--r--tests/Settings-invalid.json54
-rw-r--r--tests/Settings-invalid.ron4
-rw-r--r--tests/Settings-invalid.toml2
-rw-r--r--tests/Settings-invalid.yaml2
-rw-r--r--tests/Settings-production.toml8
-rw-r--r--tests/Settings.hjson18
-rw-r--r--tests/Settings.ini9
-rw-r--r--tests/Settings.json19
-rw-r--r--tests/Settings.json520
-rw-r--r--tests/Settings.ron20
-rw-r--r--tests/Settings.toml55
-rw-r--r--tests/Settings.yaml14
-rw-r--r--tests/Settings2.default.ini9
-rw-r--r--tests/async_builder.rs142
-rw-r--r--tests/datetime.rs110
-rw-r--r--tests/defaults.rs32
-rw-r--r--tests/empty.rs20
-rw-r--r--tests/env.rs612
-rw-r--r--tests/errors.rs155
-rw-r--r--tests/file.rs70
-rw-r--r--tests/file_ini.rs68
-rw-r--r--tests/file_json.rs113
-rw-r--r--tests/file_json5.rs91
-rw-r--r--tests/file_ron.rs91
-rw-r--r--tests/file_toml.rs103
-rw-r--r--tests/file_yaml.rs93
-rw-r--r--tests/get.rs280
-rw-r--r--tests/integer_range.rs52
-rw-r--r--tests/legacy/datetime.rs134
-rw-r--r--tests/legacy/env.rs348
-rw-r--r--tests/legacy/errors.rs134
-rw-r--r--tests/legacy/file.rs54
-rw-r--r--tests/legacy/file_ini.rs66
-rw-r--r--tests/legacy/file_json.rs113
-rw-r--r--tests/legacy/file_ron.rs90
-rw-r--r--tests/legacy/file_toml.rs102
-rw-r--r--tests/legacy/file_yaml.rs93
-rw-r--r--tests/legacy/get.rs280
-rw-r--r--tests/legacy/merge.rs60
-rw-r--r--tests/legacy/mod.rs11
-rw-r--r--tests/legacy/set.rs91
-rw-r--r--tests/legacy_tests.rs2
-rw-r--r--tests/merge.rs57
-rw-r--r--tests/set.rs91
-rw-r--r--tests/types/i64.toml1
-rw-r--r--tests/weird_keys.rs111
109 files changed, 10411 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..840597f
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "069891c14c54ca2c843ca6ddcb2354d39a1910b8"
+ },
+ "path_in_vcs": ""
+} \ No newline at end of file
diff --git a/.clippy.toml b/.clippy.toml
new file mode 100644
index 0000000..eb66960
--- /dev/null
+++ b/.clippy.toml
@@ -0,0 +1 @@
+msrv = "1.46"
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..08b88cc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8ef5278
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+- package-ecosystem: cargo
+ directory: "/"
+ schedule:
+ interval: daily
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: daily
diff --git a/.github/workflows/fixupmerge.yml b/.github/workflows/fixupmerge.yml
new file mode 100644
index 0000000..a67a91b
--- /dev/null
+++ b/.github/workflows/fixupmerge.yml
@@ -0,0 +1,12 @@
+on: [pull_request]
+
+name: Git Checks
+
+jobs:
+ block-fixup:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Block Fixup Commit Merge
+ uses: 13rac1/block-fixup-merge-action@v2.0.0
diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml
new file mode 100644
index 0000000..eb016d2
--- /dev/null
+++ b/.github/workflows/msrv.yml
@@ -0,0 +1,160 @@
+on: [push, pull_request]
+
+name: MSRV
+
+jobs:
+ check:
+ name: Check
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ rust:
+ - 1.59.0
+ - stable
+ - beta
+ - nightly
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v3
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ minimal: true
+ override: true
+
+ - name: Run cargo check
+ if: matrix.rust != 'nightly'
+ uses: actions-rs/cargo@v1
+ with:
+ command: check
+
+ - name: Run cargo check (nightly)
+ if: matrix.rust == 'nightly'
+ continue-on-error: true
+ uses: actions-rs/cargo@v1
+ with:
+ command: check
+
+ test:
+ needs: [check]
+ name: Test Suite
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ rust:
+ - 1.59.0
+ - stable
+ - beta
+ - nightly
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v3
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ minimal: true
+ override: true
+
+ - name: Run cargo test
+ if: matrix.rust != 'nightly' && matrix.rust != '1.59.0'
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --all-features
+
+ - name: Run cargo test (nightly)
+ if: matrix.rust == '1.59.0'
+ continue-on-error: true
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --tests --all-features
+
+ - name: Run cargo test (nightly)
+ if: matrix.rust == 'nightly'
+ continue-on-error: true
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --all-features
+
+ fmt:
+ needs: [check]
+ name: Rustfmt
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ rust:
+ - stable
+ - beta
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v3
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ minimal: true
+ override: true
+ components: rustfmt
+
+ - name: Run cargo fmt
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: --all -- --check
+
+ clippy:
+ needs: [check]
+ name: Clippy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v3
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.59.0
+ override: true
+ components: clippy
+
+ - name: Run cargo clippy
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: --all-targets --all-features -- -D warnings
+
+ check-examples:
+ name: Check examples
+ needs: [check]
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ rust:
+ - 1.59.0
+ - stable
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v3
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ matrix.rust }}
+ minimal: true
+ override: true
+
+ - name: Run cargo check
+ uses: actions-rs/cargo@v1
+ with:
+ command: check
+ args: --examples
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9d37c5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..bf02b37
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,30 @@
+// This file is generated by cargo2android.py --name-suffix _rust --run --features toml,json,yaml.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library_host {
+ name: "libconfig_rust",
+ crate_name: "config",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.13.3",
+ srcs: ["src/lib.rs"],
+ edition: "2018",
+ features: [
+ "json",
+ "serde_json",
+ "toml",
+ "yaml",
+ "yaml-rust",
+ ],
+ rustlibs: [
+ "liblazy_static",
+ "libnom",
+ "libpathdiff",
+ "libserde",
+ "libserde_json",
+ "libtoml",
+ "libyaml_rust",
+ ],
+ proc_macros: ["libasync_trait"],
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..917ec00
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,283 @@
+# Change Log
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+## Unreleased
+
+## 0.13.3 - 2022-12-04
+
+Please note that we had to update the MSRV for this crate from 1.56.0 to 1.59.0
+for this patch release being possible, because a transitive dependency did
+update its MSRV.
+
+ - Backport of commit [d54986c54091e4620c199d3dfadde80b82958bb3] from [#362] for
+ using float_cmp for testing floats
+ - Backport of [#379] adding `Clone` trait derive to builder states
+
+[d54986c54091e4620c199d3dfadde80b82958bb3]: https://github.com/mehcode/config-rs/commit/d54986c54091e4620c199d3dfadde80b82958bb3
+[#362]: https://github.com/mehcode/config-rs/pull/362
+[#379]: https://github.com/mehcode/config-rs/pull/379
+
+## 0.13.2 - 2022-08-02
+
+ - Backport of [#316] to be testing with temp_env. The backport was necessary to
+ be able to backport the next change. This change shouldn't be user-visible.
+ - Backport of [#353] to use TryInto for more permissive deserialization of
+ integers
+ - Backport of commit [518a3cafa1e62ba7405709e5c508247e328e0a18] from [#362] to
+ fix tests
+
+[#316]: https://github.com/mehcode/config-rs/pull/316
+[#353]: https://github.com/mehcode/config-rs/pull/353
+[518a3cafa1e62ba7405709e5c508247e328e0a18]: https://github.com/mehcode/config-rs/commit/518a3cafa1e62ba7405709e5c508247e328e0a18
+[#362]: https://github.com/mehcode/config-rs/pull/362
+
+## 0.13.1 - 2022-04-13
+
+ - typo in doc comment for ConfigBuilder [#299]
+ - dot in config file name handling fixed [#306]
+
+[#299]: https://github.com/mehcode/config-rs/pull/299
+[#306]: https://github.com/mehcode/config-rs/pull/306
+
+## 0.13.0 - 2022-04-03
+
+ - Prefix-Seperator support was added [#292]
+ - Environment lists can now be parsed [#255]
+ - Setting an overwrite from an Option was added [#303]
+ - Option to keep the prefix from an environment variable was added [#298]
+ - Some small doc/CI fixes [#307], [#309]
+ - MSRV was updated to 1.56.0 [#304]
+ - Dependencies were updated [#289], [#301]
+
+[#292]: https://github.com/mehcode/config-rs/pull/292
+[#255]: https://github.com/mehcode/config-rs/pull/255
+[#303]: https://github.com/mehcode/config-rs/pull/303
+[#298]: https://github.com/mehcode/config-rs/pull/298
+[#307]: https://github.com/mehcode/config-rs/pull/307
+[#309]: https://github.com/mehcode/config-rs/pull/309
+[#304]: https://github.com/mehcode/config-rs/pull/304
+[#289]: https://github.com/mehcode/config-rs/pull/289
+[#301]: https://github.com/mehcode/config-rs/pull/301
+
+## 0.12.0 - 2022-02-10
+
+### Format support changes in this version
+
+ - HJSON support was removed [#230]
+ - JSON5 format support [#206]
+ - RON format support [#202]
+
+### Other noteworthy changes
+
+ - A new ConfigBuilder interface for building configuration objects [#196]
+ - Asynchronous sources [#207]
+ - Custom ENV separators are now supported [#185]
+ - Loads of dependency updates and bugfixes of course
+ - Preserved map order [#217]
+ - Support for parsing numbers from the environment [#137]
+ - Support for unsigned integers [#178]
+ - `Format` trait for (custom) file formats [#219]
+
+### Deprecated
+
+ - `Environment::new()` - see [#235]
+ - Large parts of the `Config` interface - see [#196]
+ - `Config::merge()`
+ - `Config::with_merged()`
+ - `Config::refresh()`
+ - `Config::set_default()`
+ - `Config::set()`
+ - `Config::set_once()`
+ - `Config::deserialize()`
+
+[#137]: https://github.com/mehcode/config-rs/pull/137
+[#178]: https://github.com/mehcode/config-rs/pull/178
+[#185]: https://github.com/mehcode/config-rs/pull/185
+[#196]: https://github.com/mehcode/config-rs/pull/196
+[#202]: https://github.com/mehcode/config-rs/pull/202
+[#206]: https://github.com/mehcode/config-rs/pull/206
+[#207]: https://github.com/mehcode/config-rs/pull/207
+[#217]: https://github.com/mehcode/config-rs/pull/217
+[#219]: https://github.com/mehcode/config-rs/pull/219
+[#230]: https://github.com/mehcode/config-rs/pull/230
+[#235]: https://github.com/mehcode/config-rs/pull/235
+
+## 0.11.0 - 2021-03-17
+ - The `Config` type got a builder-pattern `with_merged()` method [#166].
+ - A `Config::set_once()` function was added, to set an value that can be
+ overwritten by `Config::merge`ing another configuration [#172]
+ - serde_hjson is, if enabled, pulled in without default features.
+ This is due to a bug in serde_hjson, see [#169] for more information.
+ - Testing is done on github actions [#175]
+
+[#166]: https://github.com/mehcode/config-rs/pull/166
+[#172]: https://github.com/mehcode/config-rs/pull/172
+[#169]: https://github.com/mehcode/config-rs/pull/169
+[#175]: https://github.com/mehcode/config-rs/pull/169
+
+## 0.10.1 - 2019-12-07
+ - Allow enums as configuration keys [#119]
+
+[#119]: https://github.com/mehcode/config-rs/pull/119
+
+## 0.10.0 - 2019-12-07
+ - Remove lowercasing of keys (unless the key is coming from an environment variable).
+ - Update nom to 5.x
+
+## 0.9.3 - 2019-05-09
+ - Support deserializing to a struct with `#[serde(default)]` [#106]
+
+[#106]: https://github.com/mehcode/config-rs/pull/106
+
+## 0.9.2 - 2019-01-03
+ - Support reading `enum`s from configuration. [#85]
+ - Improvements to error path (attempting to propagate path). [#89]
+ - Fix UB in monomorphic expansion. We weren't re-exporting dependent types. [#91]
+
+[#85]: https://github.com/mehcode/config-rs/pull/85
+[#89]: https://github.com/mehcode/config-rs/pull/89
+[#91]: https://github.com/mehcode/config-rs/issues/91
+
+## 0.9.1 - 2018-09-25
+ - Allow Environment variable collection to ignore empty values. [#78]
+ ```rust
+ // Empty env variables will not be collected
+ Environment::with_prefix("APP").ignore_empty(true)
+ ```
+
+[#78]: https://github.com/mehcode/config-rs/pull/78
+
+## 0.9.0 - 2018-07-02
+ - **Breaking Change:** Environment does not declare a separator by default.
+ ```rust
+ // 0.8.0
+ Environment::with_prefix("APP")
+
+ // 0.9.0
+ Environment::with_prefix("APP").separator("_")
+ ```
+
+ - Add support for INI. [#72]
+ - Add support for newtype structs. [#71]
+ - Fix bug with array set by path. [#69]
+ - Update to nom 4. [#63]
+
+[#72]: https://github.com/mehcode/config-rs/pull/72
+[#71]: https://github.com/mehcode/config-rs/pull/71
+[#69]: https://github.com/mehcode/config-rs/pull/69
+[#63]: https://github.com/mehcode/config-rs/pull/63
+
+## 0.8.0 - 2018-01-26
+ - Update lazy_static and yaml_rust
+
+## 0.7.1 - 2018-01-26
+ - Be compatible with nom's verbose_errors feature (#50)[https://github.com/mehcode/config-rs/pull/50]
+ - Add `derive(PartialEq)` for Value (#54)[https://github.com/mehcode/config-rs/pull/54]
+
+## 0.7.0 - 2017-08-05
+ - Fix conflict with `serde_yaml`. [#39]
+
+[#39]: https://github.com/mehcode/config-rs/issues/39
+
+ - Implement `Source` for `Config`.
+ - Implement `serde::de::Deserializer` for `Config`. `my_config.deserialize` may now be called as either `Deserialize::deserialize(my_config)` or `my_config.try_into()`.
+ - Remove `ConfigResult`. The builder pattern requires either `.try_into` as the final step _or_ the initial `Config::new()` to be bound to a slot. Errors must also be handled on each call instead of at the end of the chain.
+
+
+ ```rust
+ let mut c = Config::new();
+ c
+ .merge(File::with_name("Settings")).unwrap()
+ .merge(Environment::with_prefix("APP")).unwrap();
+ ```
+
+ ```rust
+ let c = Config::new()
+ .merge(File::with_name("Settings")).unwrap()
+ .merge(Environment::with_prefix("APP")).unwrap()
+ // LLVM should be smart enough to remove the actual clone operation
+ // as you are cloning a temporary that is dropped at the same time
+ .clone();
+ ```
+
+ ```rust
+ let mut s: Settings = Config::new()
+ .merge(File::with_name("Settings")).unwrap()
+ .merge(Environment::with_prefix("APP")).unwrap()
+ .try_into();
+ ```
+
+## 0.6.0 – 2017-06-22
+ - Implement `Source` for `Vec<T: Source>` and `Vec<Box<Source>>`
+
+ ```rust
+ Config::new()
+ .merge(vec![
+ File::with_name("config/default"),
+ File::with_name(&format!("config/{}", run_mode)),
+ ])
+ ```
+
+ - Implement `From<&Path>` and `From<PathBuf>` for `File`
+
+ - Remove `namespace` option for File
+ - Add builder pattern to condense configuration
+
+ ```rust
+ Config::new()
+ .merge(File::with_name("Settings"))
+ .merge(Environment::with_prefix("APP"))
+ .unwrap()
+ ```
+
+ - Parsing errors even for non required files – [@Anthony25] ( [#33] )
+
+[@Anthony25]: https://github.com/Anthony25
+[#33]: https://github.com/mehcode/config-rs/pull/33
+
+## 0.5.1 – 2017-06-16
+ - Added config category to Cargo.toml
+
+## 0.5.0 – 2017-06-16
+ - `config.get` has been changed to take a type parameter and to deserialize into that type using serde. Old behavior (get a value variant) can be used by passing `config::Value` as the type parameter: `my_config.get::<config::Value>("..")`. Some great help here from [@impowski] in [#25].
+ - Propagate parse and type errors through the deep merge (remembering filename, line, etc.)
+ - Remove directory traversal on `File`. This is likely temporary. I do _want_ this behavior but I can see how it should be optional. See [#35]
+ - Add `File::with_name` to get automatic file format detection instead of manual `FileFormat::*` – [@JordiPolo]
+ - Case normalization [#26]
+ - Remove many possible panics [#8]
+ - `my_config.refresh()` will do a full re-read from the source so live configuration is possible with some work to watch the file
+
+[#8]: https://github.com/mehcode/config-rs/issues/8
+[#35]: https://github.com/mehcode/config-rs/pull/35
+[#26]: https://github.com/mehcode/config-rs/pull/26
+[#25]: https://github.com/mehcode/config-rs/pull/25
+
+[@impowski]: https://github.com/impowski
+[@JordiPolo]: https://github.com/JordiPolo
+
+## 0.4.0 - 2017-02-12
+ - Remove global ( `config::get` ) API — It's now required to create a local configuration instance with `config::Config::new()` first.
+
+ If you'd like to have a global configuration instance, use `lazy_static!` as follows:
+
+ ```rust
+ use std::sync::RwLock;
+ use config::Config;
+
+ lazy_static! {
+ static ref CONFIG: RwLock<Config> = Default::default();
+ }
+ ```
+
+## 0.3.0 - 2017-02-08
+ - YAML from [@tmccombs](https://github.com/tmccombs)
+ - Nested field retrieval
+ - Deep merging of sources (was shallow)
+ - `config::File::from_str` to parse and merge a file from a string
+ - Support for retrieval of maps and slices — `config::get_table` and `config::get_array`
+
+## 0.2.0 - 2017-01-29
+Initial release.
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ec5343f
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,141 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "config"
+version = "0.13.3"
+authors = ["Ryan Leckey <leckey.ryan@gmail.com>"]
+description = "Layered configuration system for Rust applications."
+homepage = "https://github.com/mehcode/config-rs"
+readme = "README.md"
+keywords = [
+ "config",
+ "configuration",
+ "settings",
+ "env",
+ "environment",
+]
+categories = ["config"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/mehcode/config-rs"
+
+[dependencies.async-trait]
+version = "0.1.50"
+
+[dependencies.indexmap]
+version = "1.7.0"
+features = ["serde-1"]
+optional = true
+
+[dependencies.json5_rs]
+version = "0.4"
+optional = true
+package = "json5"
+
+[dependencies.lazy_static]
+version = "1.0"
+
+[dependencies.nom]
+version = "7"
+
+[dependencies.pathdiff]
+version = "0.2"
+
+[dependencies.ron]
+version = "0.7"
+optional = true
+
+[dependencies.rust-ini]
+version = "0.18"
+optional = true
+
+[dependencies.serde]
+version = "1.0.8"
+
+[dependencies.serde_json]
+version = "1.0.2"
+optional = true
+
+[dependencies.toml]
+version = "0.5"
+optional = true
+
+[dependencies.yaml-rust]
+version = "0.4"
+optional = true
+
+[dev-dependencies.chrono]
+version = "0.4"
+features = ["serde"]
+
+[dev-dependencies.float-cmp]
+version = "0.9"
+
+[dev-dependencies.futures]
+version = "0.3.15"
+
+[dev-dependencies.glob]
+version = "0.3"
+
+[dev-dependencies.lazy_static]
+version = "1"
+
+[dev-dependencies.notify]
+version = "^4.0.0"
+
+[dev-dependencies.reqwest]
+version = "0.11.10"
+
+[dev-dependencies.serde]
+version = "1.0"
+
+[dev-dependencies.serde_derive]
+version = "1.0.8"
+
+[dev-dependencies.temp-env]
+version = "0.2.0"
+
+[dev-dependencies.tokio]
+version = "1"
+features = [
+ "rt-multi-thread",
+ "macros",
+ "fs",
+ "io-util",
+ "time",
+]
+
+[dev-dependencies.warp]
+version = "=0.3.1"
+
+[features]
+default = [
+ "toml",
+ "json",
+ "yaml",
+ "ini",
+ "ron",
+ "json5",
+]
+ini = ["rust-ini"]
+json = ["serde_json"]
+json5 = ["json5_rs"]
+preserve_order = [
+ "indexmap",
+ "toml/preserve_order",
+ "serde_json/preserve_order",
+ "ron/indexmap",
+]
+yaml = ["yaml-rust"]
+
+[badges.maintenance]
+status = "actively-developed"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..3b81dca
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,53 @@
+[package]
+name = "config"
+version = "0.13.3"
+description = "Layered configuration system for Rust applications."
+homepage = "https://github.com/mehcode/config-rs"
+repository = "https://github.com/mehcode/config-rs"
+readme = "README.md"
+keywords = ["config", "configuration", "settings", "env", "environment"]
+authors = ["Ryan Leckey <leckey.ryan@gmail.com>"]
+categories = ["config"]
+license = "MIT/Apache-2.0"
+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"
+
+toml = { version = "0.5", 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"
+
+serde = "1.0"
+glob = "0.3"
+lazy_static = "1"
+notify = "^4.0.0"
+temp-env = "0.2.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1d2984b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,230 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2017 Ryan Leckey
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+---
+
+Copyright (c) 2017 Ryan Leckey
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..88c55e3
--- /dev/null
+++ b/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2017 Ryan Leckey
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. \ No newline at end of file
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..e8c44de
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright (c) 2017 Ryan Leckey
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..947cce6
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "config"
+description: "Layered configuration system for Rust applications."
+third_party {
+ identifier {
+ type: "crates.io"
+ value: "https://crates.io/crates/config"
+ }
+ identifier {
+ type: "Archive"
+ value: "https://static.crates.io/crates/config/config-0.13.3.crate"
+ }
+ version: "0.13.3"
+ # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 8
+ day: 22
+ }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18f0914
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# config-rs
+
+![Rust](https://img.shields.io/badge/rust-stable-brightgreen.svg)
+[![Build Status](https://travis-ci.org/mehcode/config-rs.svg?branch=master)](https://travis-ci.org/mehcode/config-rs)
+[![Crates.io](https://img.shields.io/crates/d/config.svg)](https://crates.io/crates/config)
+[![Docs.rs](https://docs.rs/config/badge.svg)](https://docs.rs/config)
+
+> Layered configuration system for Rust applications (with strong support for [12-factor] applications).
+
+[12-factor]: https://12factor.net/config
+
+ - Set defaults
+ - Set explicit values (to programmatically override)
+ - Read from [JSON], [TOML], [YAML], [INI], [RON], [JSON5] files
+ - Read from environment
+ - Loosely typed — Configuration values may be read in any supported type, as long as there exists a reasonable conversion
+ - Access nested fields using a formatted path — Uses a subset of JSONPath; currently supports the child ( `redis.port` ) and subscript operators ( `databases[0].name` )
+
+[JSON]: https://github.com/serde-rs/json
+[TOML]: https://github.com/toml-lang/toml
+[YAML]: https://github.com/chyh1990/yaml-rust
+[INI]: https://github.com/zonyitoo/rust-ini
+[RON]: https://github.com/ron-rs/ron
+[JSON5]: https://github.com/callum-oakley/json5-rs
+
+Please note that this library can not be used to write changed configuration
+values back to the configuration file(s)!
+
+## Usage
+
+```toml
+[dependencies]
+config = "0.13.1"
+```
+
+### Feature flags
+
+ - `ini` - Adds support for reading INI files
+ - `json` - Adds support for reading JSON files
+ - `yaml` - Adds support for reading YAML files
+ - `toml` - Adds support for reading TOML files
+ - `ron` - Adds support for reading RON files
+ - `json5` - Adds support for reading JSON5 files
+
+### Support for custom formats
+
+Library provides out of the box support for most renowned data formats such as JSON or Yaml. Nonetheless, it contains an extensibility point - a `Format` trait that, once implemented, allows seamless integration with library's APIs using custom, less popular or proprietary data formats.
+
+See [custom_format](https://github.com/mehcode/config-rs/tree/master/examples/custom_format) example for more information.
+
+### More
+
+See the [documentation](https://docs.rs/config) or [examples](https://github.com/mehcode/config-rs/tree/master/examples) for
+more usage information.
+
+
+## MSRV
+
+We currently support Rust 1.56.0 and newer.
+
+
+## License
+
+config-rs is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).
+
+See LICENSE-APACHE and LICENSE-MIT for details.
diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs
new file mode 100644
index 0000000..f5459b9
--- /dev/null
+++ b/examples/async_source/main.rs
@@ -0,0 +1,74 @@
+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<dyn Error>> {
+ select! {
+ r = run_server().fuse() => r,
+ r = run_client().fuse() => r
+ }
+}
+
+async fn run_server() -> Result<(), Box<dyn Error>> {
+ 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<dyn Error>> {
+ // Good enough for an example to allow server to start
+ tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
+
+ let config = ConfigBuilder::<AsyncState>::default()
+ .add_async_source(HttpSource {
+ uri: "http://localhost:5001/configuration".into(),
+ format: FileFormat::Json,
+ })
+ .build()
+ .await?;
+
+ println!("Config value is {}", config.get::<String>("value")?);
+
+ Ok(())
+}
+
+// Actual implementation of AsyncSource can be found below
+
+#[derive(Debug)]
+struct HttpSource<F: Format> {
+ uri: String,
+ format: F,
+}
+
+#[async_trait]
+impl<F: Format + Send + Sync + Debug> AsyncSource for HttpSource<F> {
+ async fn collect(&self) -> Result<Map<String, config::Value>, ConfigError> {
+ reqwest::get(&self.uri)
+ .await
+ .map_err(|e| ConfigError::Foreign(Box::new(e)))? // error conversion is possible from custom AsyncSource impls
+ .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
new file mode 100644
index 0000000..4da2e9d
--- /dev/null
+++ b/examples/custom_format/main.rs
@@ -0,0 +1,50 @@
+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<Map<String, config::Value>, Box<dyn std::error::Error + Send + Sync>> {
+ // 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
new file mode 100644
index 0000000..f567419
--- /dev/null
+++ b/examples/env-list/main.rs
@@ -0,0 +1,25 @@
+use config::Config;
+#[derive(Debug, Default, serde_derive::Deserialize, PartialEq)]
+struct AppConfig {
+ list: Vec<String>,
+}
+
+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
new file mode 100644
index 0000000..7b95e7a
--- /dev/null
+++ b/examples/glob/conf/00-default.toml
@@ -0,0 +1 @@
+debug = false
diff --git a/examples/glob/conf/05-some.yml b/examples/glob/conf/05-some.yml
new file mode 100644
index 0000000..52555a0
--- /dev/null
+++ b/examples/glob/conf/05-some.yml
@@ -0,0 +1,2 @@
+secret: THIS IS SECRET
+debug: true
diff --git a/examples/glob/conf/99-extra.json b/examples/glob/conf/99-extra.json
new file mode 100644
index 0000000..26f908c
--- /dev/null
+++ b/examples/glob/conf/99-extra.json
@@ -0,0 +1,5 @@
+{
+ "that": 3,
+ "this": 1230,
+ "key": "sdgnjklsdjklgds"
+}
diff --git a/examples/glob/main.rs b/examples/glob/main.rs
new file mode 100644
index 0000000..d5225a0
--- /dev/null
+++ b/examples/glob/main.rs
@@ -0,0 +1,66 @@
+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::<HashMap<String, String>>()
+ .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::<HashMap<String, String>>()
+ .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::<Vec<_>>(),
+ )
+ .build()
+ .unwrap();
+
+ // Print out our settings (as a HashMap)
+ println!(
+ "\n{:?} \n\n-----------",
+ settings
+ .try_deserialize::<HashMap<String, String>>()
+ .unwrap()
+ );
+}
diff --git a/examples/global/main.rs b/examples/global/main.rs
new file mode 100644
index 0000000..160d881
--- /dev/null
+++ b/examples/global/main.rs
@@ -0,0 +1,23 @@
+#![allow(deprecated)]
+use config::Config;
+use lazy_static::lazy_static;
+use std::error::Error;
+use std::sync::RwLock;
+
+lazy_static! {
+ static ref SETTINGS: RwLock<Config> = RwLock::new(Config::default());
+}
+
+fn try_main() -> Result<(), Box<dyn Error>> {
+ // Set property
+ SETTINGS.write()?.set("property", 42)?;
+
+ // Get property
+ println!("property: {}", SETTINGS.read()?.get::<i32>("property")?);
+
+ Ok(())
+}
+
+fn main() {
+ try_main().unwrap();
+}
diff --git a/examples/hierarchical-env/config/default.toml b/examples/hierarchical-env/config/default.toml
new file mode 100644
index 0000000..dbb2f30
--- /dev/null
+++ b/examples/hierarchical-env/config/default.toml
@@ -0,0 +1,17 @@
+[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
new file mode 100644
index 0000000..f07dcc2
--- /dev/null
+++ b/examples/hierarchical-env/config/development.toml
@@ -0,0 +1,4 @@
+debug = true
+
+[database]
+echo = true
diff --git a/examples/hierarchical-env/config/production.toml b/examples/hierarchical-env/config/production.toml
new file mode 100644
index 0000000..cd0c4cf
--- /dev/null
+++ b/examples/hierarchical-env/config/production.toml
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 0000000..ee1b69b
--- /dev/null
+++ b/examples/hierarchical-env/main.rs
@@ -0,0 +1,10 @@
+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
new file mode 100644
index 0000000..65b5f87
--- /dev/null
+++ b/examples/hierarchical-env/settings.rs
@@ -0,0 +1,76 @@
+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<Self, ConfigError> {
+ 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::<String>("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
new file mode 100644
index 0000000..fd6d3c6
--- /dev/null
+++ b/examples/simple/Settings.toml
@@ -0,0 +1,3 @@
+debug = false
+priority = 32
+key = "189rjfadoisfj8923fjio"
diff --git a/examples/simple/main.rs b/examples/simple/main.rs
new file mode 100644
index 0000000..55ae8ac
--- /dev/null
+++ b/examples/simple/main.rs
@@ -0,0 +1,21 @@
+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::<HashMap<String, String>>()
+ .unwrap()
+ );
+}
diff --git a/examples/watch/Settings.toml b/examples/watch/Settings.toml
new file mode 100644
index 0000000..1518068
--- /dev/null
+++ b/examples/watch/Settings.toml
@@ -0,0 +1,3 @@
+debug = false
+port = 3223
+host = "0.0.0.0"
diff --git a/examples/watch/main.rs b/examples/watch/main.rs
new file mode 100644
index 0000000..801ee4f
--- /dev/null
+++ b/examples/watch/main.rs
@@ -0,0 +1,70 @@
+#![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<Config> = 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::<HashMap<String, String>>()
+ .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/builder.rs b/src/builder.rs
new file mode 100644
index 0000000..ee266d4
--- /dev/null
+++ b/src/builder.rs
@@ -0,0 +1,370 @@
+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<dyn Error>> {
+/// 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<dyn Error>> {
+/// 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::<AsyncState>::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::<DefaultState>::default();
+/// ```
+#[derive(Debug, Clone, Default)]
+pub struct ConfigBuilder<St: BuilderState> {
+ defaults: Map<Expression, Value>,
+ overrides: Map<Expression, Value>,
+ state: St,
+}
+
+/// Represents [`ConfigBuilder`] state.
+pub trait BuilderState {}
+
+/// Represents data specific to builder in default, sychronous state, without support for async.
+#[derive(Debug, Default, Clone)]
+pub struct DefaultState {
+ sources: Vec<Box<dyn Source + Send + Sync>>,
+}
+
+/// 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, Clone)]
+pub struct AsyncState {
+ sources: Vec<SourceType>,
+}
+
+#[derive(Debug, Clone)]
+enum SourceType {
+ Sync(Box<dyn Source + Send + Sync>),
+ Async(Box<dyn AsyncSource + Send + Sync>),
+}
+
+impl BuilderState for DefaultState {}
+impl BuilderState for AsyncState {}
+
+impl<St: BuilderState> ConfigBuilder<St> {
+ // 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<S, T>(mut self, key: S, value: T) -> Result<Self>
+ where
+ S: AsRef<str>,
+ T: Into<Value>,
+ {
+ 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<S, T>(mut self, key: S, value: T) -> Result<Self>
+ where
+ S: AsRef<str>,
+ T: Into<Value>,
+ {
+ 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<S, T>(mut self, key: S, value: Option<T>) -> Result<Self>
+ where
+ S: AsRef<str>,
+ T: Into<Value>,
+ {
+ if let Some(value) = value {
+ self.overrides
+ .insert(Expression::from_str(key.as_ref())?, value.into());
+ }
+ Ok(self)
+ }
+}
+
+impl ConfigBuilder<DefaultState> {
+ // 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<T>(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<T>(self, source: T) -> ConfigBuilder<AsyncState>
+ 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<Config> {
+ 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<Config> {
+ Self::build_internal(
+ self.defaults.clone(),
+ self.overrides.clone(),
+ &self.state.sources,
+ )
+ }
+
+ fn build_internal(
+ defaults: Map<Expression, Value>,
+ overrides: Map<Expression, Value>,
+ sources: &[Box<dyn Source + Send + Sync>],
+ ) -> Result<Config> {
+ let mut cache: Value = Map::<String, Value>::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<AsyncState> {
+ // 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<T>(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<T>(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<Config> {
+ 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<Config> {
+ Self::build_internal(
+ self.defaults.clone(),
+ self.overrides.clone(),
+ &self.state.sources,
+ )
+ .await
+ }
+
+ async fn build_internal(
+ defaults: Map<Expression, Value>,
+ overrides: Map<Expression, Value>,
+ sources: &[SourceType],
+ ) -> Result<Config> {
+ let mut cache: Value = Map::<String, Value>::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
new file mode 100644
index 0000000..27b318d
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,218 @@
+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<path::Expression, Value>,
+ overrides: Map<path::Expression, Value>,
+ sources: Vec<Box<dyn Source + Send + Sync>>,
+
+ /// 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<DefaultState> {
+ ConfigBuilder::<DefaultState>::default()
+ }
+
+ /// Merge in a configuration property source.
+ #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")]
+ pub fn merge<T>(&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<T>(mut self, source: T) -> Result<Self>
+ 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::<String, Value>::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<T>(&mut self, key: &str, value: T) -> Result<&mut Self>
+ where
+ T: Into<Value>,
+ {
+ 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<T>(&mut self, key: &str, value: T) -> Result<&mut Self>
+ where
+ T: Into<Value>,
+ {
+ 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<T> {
+ // 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<String> {
+ self.get(key).and_then(Value::into_string)
+ }
+
+ pub fn get_int(&self, key: &str) -> Result<i64> {
+ self.get(key).and_then(Value::into_int)
+ }
+
+ pub fn get_float(&self, key: &str) -> Result<f64> {
+ self.get(key).and_then(Value::into_float)
+ }
+
+ pub fn get_bool(&self, key: &str) -> Result<bool> {
+ self.get(key).and_then(Value::into_bool)
+ }
+
+ pub fn get_table(&self, key: &str) -> Result<Map<String, Value>> {
+ self.get(key).and_then(Value::into_table)
+ }
+
+ pub fn get_array(&self, key: &str) -> Result<Vec<Value>> {
+ 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> {
+ T::deserialize(self)
+ }
+
+ /// Attempt to serialize the entire configuration from the given type.
+ pub fn try_from<T: Serialize>(from: &T) -> Result<Self> {
+ 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<T> {
+ self.try_deserialize()
+ }
+}
+
+impl Source for Config {
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new((*self).clone())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ self.cache.clone().into_table()
+ }
+}
diff --git a/src/de.rs b/src/de.rs
new file mode 100644
index 0000000..d1271b2
--- /dev/null
+++ b/src/de.rs
@@ -0,0 +1,468 @@
+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<V>(self, visitor: V) -> Result<V::Value>
+ 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_bool(self.into_bool()?)
+ }
+
+ #[inline]
+ fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_i64(self.into_int()?)
+ }
+
+ #[inline]
+ fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u8(self.into_uint()? as u8)
+ }
+
+ #[inline]
+ fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u16(self.into_uint()? as u16)
+ }
+
+ #[inline]
+ fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u32(self.into_uint()? as u32)
+ }
+
+ #[inline]
+ fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // FIXME: This should *fail* if the value does not fit in the requets integer type
+ visitor.visit_u64(self.into_uint()? as u64)
+ }
+
+ #[inline]
+ fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f32(self.into_float()? as f32)
+ }
+
+ #[inline]
+ fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f64(self.into_float()?)
+ }
+
+ #[inline]
+ fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.into_string()?)
+ }
+
+ #[inline]
+ fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.into_string()?)
+ }
+
+ #[inline]
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
+ 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<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
+ where
+ V: de::Visitor<'de>,
+ {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_enum<V>(
+ self,
+ name: &'static str,
+ variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value>
+ 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ 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<Value>>,
+}
+
+impl SeqAccess {
+ fn new(elements: Vec<Value>) -> Self {
+ Self {
+ elements: elements.into_iter().enumerate(),
+ }
+ }
+}
+
+impl<'de> de::SeqAccess<'de> for SeqAccess {
+ type Error = ConfigError;
+
+ fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
+ 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<usize> {
+ 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<String, Value>) -> Self {
+ Self {
+ elements: table.into_iter().collect(),
+ }
+ }
+}
+
+impl<'de> de::MapAccess<'de> for MapAccess {
+ type Error = ConfigError;
+
+ fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
+ 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<V>(&mut self, seed: V) -> Result<V::Value>
+ 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<StrDeserializer> {
+ 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<StrDeserializer> {
+ 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<V>(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<T>(self, seed: T) -> Result<T::Value>
+ where
+ T: de::DeserializeSeed<'de>,
+ {
+ match self.value.kind {
+ ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1),
+ _ => unreachable!(),
+ }
+ }
+
+ fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
+ 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<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
+ 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<V>(self, visitor: V) -> Result<V::Value>
+ 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_bool(self.cache.into_bool()?)
+ }
+
+ #[inline]
+ fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_i64(self.cache.into_int()?)
+ }
+
+ #[inline]
+ fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // 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<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f32(self.cache.into_float()? as f32)
+ }
+
+ #[inline]
+ fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f64(self.cache.into_float()?)
+ }
+
+ #[inline]
+ fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.cache.into_string()?)
+ }
+
+ #[inline]
+ fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_string(self.cache.into_string()?)
+ }
+
+ #[inline]
+ fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
+ 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<V>(
+ self,
+ name: &'static str,
+ variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value>
+ 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/env.rs b/src/env.rs
new file mode 100644
index 0000000..432df2c
--- /dev/null
+++ b/src/env.rs
@@ -0,0 +1,244 @@
+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<String>,
+
+ /// Optional character sequence that separates the prefix from the rest of the key
+ prefix_separator: Option<String>,
+
+ /// 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<String>,
+
+ /// 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<String>,
+ /// A list of keys which should always be parsed as a list. If not set you can have only Vec<String> or String (not both) in one environment.
+ list_parse_keys: Option<Vec<String>>,
+
+ /// 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<Map<String, String>>,
+}
+
+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<String>`] instead of [`String`].
+ /// See [`with_list_parse_key`] when you want to use [`Vec<String>`] 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<String>`].
+ /// To switch the default type back to type Strings you need to provide the keys which should be [`Vec<String>`] 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<Map<String, String>>) -> Self {
+ self.source = source;
+ self
+ }
+}
+
+impl Source for Environment {
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new((*self).clone())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ let mut m = Map::new();
+ let uri: String = "the environment".into();
+
+ let separator = self.separator.as_deref().unwrap_or("");
+ 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::<bool>() {
+ ValueKind::Boolean(parsed)
+ } else if let Ok(parsed) = value.parse::<i64>() {
+ ValueKind::I64(parsed)
+ } else if let Ok(parsed) = value.parse::<f64>() {
+ 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> = 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> = 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
new file mode 100644
index 0000000..f955a28
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,244 @@
+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, "integer 64 bit `{}`", i),
+ Unexpected::I128(i) => write!(f, "integer 128 bit `{}`", i),
+ Unexpected::U64(i) => write!(f, "unsigned integer 64 bit `{}`", i),
+ Unexpected::U128(i) => write!(f, "unsigned integer 128 bit `{}`", 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<String>,
+
+ /// 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<dyn Error + Send + Sync>,
+ },
+
+ /// 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<String>,
+
+ /// 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<String>,
+ },
+
+ /// Custom message
+ Message(String),
+
+ /// Unadorned error from a foreign origin.
+ Foreign(Box<dyn Error + Send + Sync>),
+}
+
+impl ConfigError {
+ // FIXME: pub(crate)
+ #[doc(hidden)]
+ pub fn invalid_type(
+ origin: Option<String>,
+ 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<Self> {
+ 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<String>| {
+ 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<T> = result::Result<T, ConfigError>;
+
+// 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<T: fmt::Display>(msg: T) -> Self {
+ Self::Message(msg.to_string())
+ }
+}
+
+impl ser::Error for ConfigError {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Self::Message(msg.to_string())
+ }
+}
diff --git a/src/file/format/ini.rs b/src/file/format/ini.rs
new file mode 100644
index 0000000..9295e60
--- /dev/null
+++ b/src/file/format/ini.rs
@@ -0,0 +1,37 @@
+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<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ let mut map: Map<String, Value> = Map::new();
+ let i = Ini::load_from_str(text)?;
+ for (sec, prop) in i.iter() {
+ match sec {
+ Some(sec) => {
+ let mut sec_map: Map<String, Value> = 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
new file mode 100644
index 0000000..1720cb6
--- /dev/null
+++ b/src/file/format/json.rs
@@ -0,0 +1,58 @@
+use std::error::Error;
+
+use crate::map::Map;
+use crate::value::{Value, ValueKind};
+
+pub fn parse(
+ uri: Option<&String>,
+ text: &str,
+) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ // Parse a JSON object value from the text
+ // TODO: Have a proper error fire if the root of a file is ever not a Table
+ let value = from_json_value(uri, &serde_json::from_str(text)?);
+ match value.kind {
+ ValueKind::Table(map) => Ok(map),
+
+ _ => Ok(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
new file mode 100644
index 0000000..92aaa8f
--- /dev/null
+++ b/src/file/format/json5.rs
@@ -0,0 +1,66 @@
+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<Self>),
+ Object(Map<String, Self>),
+}
+
+pub fn parse(
+ uri: Option<&String>,
+ text: &str,
+) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ match json5_rs::from_str::<Val>(text)? {
+ Val::String(ref value) => Err(Unexpected::Str(value.clone())),
+ Val::Integer(value) => Err(Unexpected::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<dyn Error + Send + Sync>)
+}
+
+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
new file mode 100644
index 0000000..025e98a
--- /dev/null
+++ b/src/file/format/mod.rs
@@ -0,0 +1,147 @@
+// 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<FileFormat, Vec<&'static str>> = {
+ let mut formats: HashMap<FileFormat, Vec<_>> = 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<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ 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<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ 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
new file mode 100644
index 0000000..fb2b063
--- /dev/null
+++ b/src/file/format/ron.rs
@@ -0,0 +1,66 @@
+use std::error::Error;
+
+use crate::map::Map;
+use crate::value::{Value, ValueKind};
+
+pub fn parse(
+ uri: Option<&String>,
+ text: &str,
+) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ let value = from_ron_value(uri, ron::from_str(text)?)?;
+ match value.kind {
+ ValueKind::Table(map) => Ok(map),
+
+ _ => Ok(Map::new()),
+ }
+}
+
+fn from_ron_value(
+ uri: Option<&String>,
+ value: ron::Value,
+) -> Result<Value, Box<dyn Error + Send + Sync>> {
+ 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::<Result<Vec<_>, _>>()?;
+
+ ValueKind::Array(array)
+ }
+
+ ron::Value::Map(values) => {
+ let map = values
+ .iter()
+ .map(|(key, value)| -> Result<_, Box<dyn Error + Send + Sync>> {
+ let key = key.clone().into_rust::<String>()?;
+ let value = from_ron_value(uri, value.clone())?;
+
+ Ok((key, value))
+ })
+ .collect::<Result<Map<_, _>, _>>()?;
+
+ ValueKind::Table(map)
+ }
+ };
+
+ Ok(Value::new(uri, kind))
+}
diff --git a/src/file/format/toml.rs b/src/file/format/toml.rs
new file mode 100644
index 0000000..af21fc7
--- /dev/null
+++ b/src/file/format/toml.rs
@@ -0,0 +1,49 @@
+use std::error::Error;
+
+use crate::map::Map;
+use crate::value::{Value, ValueKind};
+
+pub fn parse(
+ uri: Option<&String>,
+ text: &str,
+) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ // Parse a TOML value from the provided text
+ // TODO: Have a proper error fire if the root of a file is ever not a Table
+ let value = from_toml_value(uri, &toml::from_str(text)?);
+ match value.kind {
+ ValueKind::Table(map) => Ok(map),
+
+ _ => Ok(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
new file mode 100644
index 0000000..2a76261
--- /dev/null
+++ b/src/file/format/yaml.rs
@@ -0,0 +1,108 @@
+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<Map<String, Value>, Box<dyn Error + Send + Sync>> {
+ // Parse a YAML object from file
+ let mut docs = yaml::YamlLoader::load_from_str(text)?;
+ let root = match docs.len() {
+ 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<Value, Box<dyn Error + Send + Sync>> {
+ 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::<f64>()
+ .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
new file mode 100644
index 0000000..65f3fd6
--- /dev/null
+++ b/src/file/mod.rs
@@ -0,0 +1,149 @@
+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<T, F> {
+ source: T,
+
+ /// Format of file (which dictates what driver to use).
+ format: Option<F>,
+
+ /// 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<F> File<source::string::FileSourceString, F>
+where
+ F: FileStoredFormat + 'static,
+{
+ pub fn from_str(s: &str, format: F) -> Self {
+ Self {
+ format: Some(format),
+ required: true,
+ source: s.into(),
+ }
+ }
+}
+
+impl<F> File<source::file::FileSourceFile, F>
+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<source::file::FileSourceFile, FileFormat> {
+ /// 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<source::file::FileSourceFile, FileFormat> {
+ fn from(path: &'a Path) -> Self {
+ Self {
+ format: None,
+ required: true,
+ source: source::file::FileSourceFile::new(path.to_path_buf()),
+ }
+ }
+}
+
+impl From<PathBuf> for File<source::file::FileSourceFile, FileFormat> {
+ fn from(path: PathBuf) -> Self {
+ Self {
+ format: None,
+ required: true,
+ source: source::file::FileSourceFile::new(path),
+ }
+ }
+}
+
+impl<T, F> File<T, F>
+where
+ F: FileStoredFormat + 'static,
+ T: FileSource<F>,
+{
+ #[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<T, F> Source for File<T, F>
+where
+ F: FileStoredFormat + Debug + Clone + Send + Sync + 'static,
+ T: Sync + Send + FileSource<F> + 'static,
+{
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new((*self).clone())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ // 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
new file mode 100644
index 0000000..8ee5d31
--- /dev/null
+++ b/src/file/source/file.rs
@@ -0,0 +1,141 @@
+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<F>(
+ &self,
+ format_hint: Option<F>,
+ ) -> Result<(PathBuf, Box<dyn Format>), Box<dyn Error + Send + Sync>>
+ 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<F> FileSource<F> for FileSourceFile
+where
+ F: Format + FileStoredFormat + 'static,
+{
+ fn resolve(
+ &self,
+ format_hint: Option<F>,
+ ) -> Result<FileSourceResult, Box<dyn Error + Send + Sync>> {
+ // 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
new file mode 100644
index 0000000..3c3d10c
--- /dev/null
+++ b/src/file/source/mod.rs
@@ -0,0 +1,38 @@
+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<T>: Debug + Clone
+where
+ T: Format + FileStoredFormat,
+{
+ fn resolve(
+ &self,
+ format_hint: Option<T>,
+ ) -> Result<FileSourceResult, Box<dyn Error + Send + Sync>>;
+}
+
+pub struct FileSourceResult {
+ pub(crate) uri: Option<String>,
+ pub(crate) content: String,
+ pub(crate) format: Box<dyn Format>,
+}
+
+impl FileSourceResult {
+ pub fn uri(&self) -> &Option<String> {
+ &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
new file mode 100644
index 0000000..89300d4
--- /dev/null
+++ b/src/file/source/string.rs
@@ -0,0 +1,33 @@
+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<F> FileSource<F> for FileSourceString
+where
+ F: Format + FileStoredFormat + 'static,
+{
+ fn resolve(
+ &self,
+ format_hint: Option<F>,
+ ) -> Result<FileSourceResult, Box<dyn Error + Send + Sync>> {
+ 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
new file mode 100644
index 0000000..ab9e4fa
--- /dev/null
+++ b/src/format.rs
@@ -0,0 +1,23 @@
+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<Map<String, Value>, Box<dyn Error + Send + Sync>>;
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..589d2d5
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,44 @@
+//! 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 config;
+mod de;
+mod env;
+mod error;
+mod file;
+mod format;
+mod map;
+mod path;
+mod ser;
+mod source;
+mod value;
+
+pub use crate::builder::{AsyncConfigBuilder, ConfigBuilder};
+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};
diff --git a/src/map.rs b/src/map.rs
new file mode 100644
index 0000000..5873f0d
--- /dev/null
+++ b/src/map.rs
@@ -0,0 +1,4 @@
+#[cfg(not(feature = "preserve_order"))]
+pub type Map<K, V> = std::collections::HashMap<K, V>;
+#[cfg(feature = "preserve_order")]
+pub type Map<K, V> = indexmap::IndexMap<K, V>;
diff --git a/src/path/mod.rs b/src/path/mod.rs
new file mode 100644
index 0000000..7456aa3
--- /dev/null
+++ b/src/path/mod.rs
@@ -0,0 +1,250 @@
+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<Self>, String),
+ Subscript(Box<Self>, isize),
+}
+
+impl FromStr for Expression {
+ type Err = ConfigError;
+
+ fn from_str(s: &str) -> Result<Self> {
+ 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::<String, Value>::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::<Value>::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::<String, Value>::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::<String, Value>::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::<String, Value>::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::<Value>::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
new file mode 100644
index 0000000..8378121
--- /dev/null
+++ b/src/path/parser.rs
@@ -0,0 +1,131 @@
+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<Expression, ErrorKind> {
+ 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<nom::error::Error<&str>>) -> 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
new file mode 100644
index 0000000..9ccf7c7
--- /dev/null
+++ b/src/ser.rs
@@ -0,0 +1,720 @@
+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<usize>)>,
+ pub output: Config,
+}
+
+impl ConfigSerializer {
+ fn serialize_primitive<T>(&mut self, value: T) -> Result<()>
+ where
+ T: Into<Value> + 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<usize>)> {
+ 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<usize>)> {
+ 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::Ok> {
+ self.serialize_primitive(v)
+ }
+
+ fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
+ self.serialize_i64(v.into())
+ }
+
+ fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
+ self.serialize_i64(v.into())
+ }
+
+ fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
+ self.serialize_i64(v.into())
+ }
+
+ fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
+ self.serialize_primitive(v)
+ }
+
+ fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
+ self.serialize_u64(v.into())
+ }
+
+ fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
+ self.serialize_u64(v.into())
+ }
+
+ fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
+ self.serialize_u64(v.into())
+ }
+
+ fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
+ 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::Ok> {
+ self.serialize_f64(v.into())
+ }
+
+ fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
+ self.serialize_primitive(v)
+ }
+
+ fn serialize_char(self, v: char) -> Result<Self::Ok> {
+ self.serialize_primitive(v.to_string())
+ }
+
+ fn serialize_str(self, v: &str) -> Result<Self::Ok> {
+ self.serialize_primitive(v.to_string())
+ }
+
+ fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok> {
+ 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::Ok> {
+ self.serialize_unit()
+ }
+
+ fn serialize_some<T>(self, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok> {
+ self.serialize_primitive(Value::from(ValueKind::Nil))
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok> {
+ self.serialize_unit()
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Self::Ok> {
+ self.serialize_str(variant)
+ }
+
+ fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ value: &T,
+ ) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ self.push_key(variant);
+ value.serialize(&mut *self)?;
+ self.pop_key();
+ Ok(())
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Ok(self)
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> Result<Self::SerializeTupleStruct> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeTupleVariant> {
+ self.push_key(variant);
+ Ok(self)
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Ok(self)
+ }
+
+ fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
+ self.serialize_map(Some(len))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ _len: usize,
+ ) -> Result<Self::SerializeStructVariant> {
+ self.push_key(variant);
+ Ok(self)
+ }
+}
+
+impl<'a> ser::SerializeSeq for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_element<T>(&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::Ok> {
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeTuple for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_element<T>(&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::Ok> {
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeTupleStruct for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&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::Ok> {
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeTupleVariant for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&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::Ok> {
+ self.pop_key();
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeMap for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_key<T>(&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<T>(&mut self, value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(&mut **self)?;
+ self.pop_key();
+ Ok(())
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeStruct for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&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::Ok> {
+ Ok(())
+ }
+}
+
+impl<'a> ser::SerializeStructVariant for &'a mut ConfigSerializer {
+ type Ok = ();
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&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::Ok> {
+ 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<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_char(self, v: char) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_str(self, v: &str) -> Result<Self::Ok> {
+ Ok(v.to_string())
+ }
+
+ fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok> {
+ Ok(String::from_utf8_lossy(v).to_string())
+ }
+
+ fn serialize_none(self) -> Result<Self::Ok> {
+ self.serialize_unit()
+ }
+
+ fn serialize_some<T>(self, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Self::Ok> {
+ Ok(String::new())
+ }
+
+ fn serialize_unit_struct(self, _name: &str) -> Result<Self::Ok> {
+ self.serialize_unit()
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &str,
+ _variant_index: u32,
+ variant: &str,
+ ) -> Result<Self::Ok> {
+ Ok(variant.to_string())
+ }
+
+ fn serialize_newtype_struct<T>(self, _name: &str, value: &T) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T>(
+ self,
+ _name: &str,
+ _variant_index: u32,
+ _variant: &str,
+ value: &T,
+ ) -> Result<Self::Ok>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ value.serialize(self)
+ }
+
+ fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+ Err(ConfigError::Message(
+ "seq can't serialize to string key".to_string(),
+ ))
+ }
+
+ fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
+ Err(ConfigError::Message(
+ "tuple can't serialize to string key".to_string(),
+ ))
+ }
+
+ fn serialize_tuple_struct(self, name: &str, _len: usize) -> Result<Self::SerializeTupleStruct> {
+ 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<Self::SerializeTupleVariant> {
+ Err(ConfigError::Message(format!(
+ "tuple variant {}::{} can't serialize to string key",
+ name, variant
+ )))
+ }
+
+ fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+ Err(ConfigError::Message(
+ "map can't serialize to string key".to_string(),
+ ))
+ }
+
+ fn serialize_struct(self, name: &str, _len: usize) -> Result<Self::SerializeStruct> {
+ 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<Self::SerializeStructVariant> {
+ 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<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeTuple for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_element<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeTupleStruct for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeTupleVariant for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeMap for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_key<T>(&mut self, _key: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn serialize_value<T>(&mut self, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeStruct for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ unreachable!()
+ }
+}
+
+impl ser::SerializeStructVariant for StringKeySerializer {
+ type Ok = String;
+ type Error = ConfigError;
+
+ fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<()>
+ where
+ T: ?Sized + ser::Serialize,
+ {
+ unreachable!()
+ }
+
+ fn end(self) -> Result<Self::Ok> {
+ 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<String>,
+ }
+
+ 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
new file mode 100644
index 0000000..94f1c35
--- /dev/null
+++ b/src/source.rs
@@ -0,0 +1,145 @@
+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<dyn Source + Send + Sync>;
+
+ /// Collect all configuration properties available from this source and return
+ /// a Map.
+ fn collect(&self) -> Result<Map<String, Value>>;
+
+ /// 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<Map<String, Value>>;
+
+ /// 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<dyn AsyncSource + Send + Sync> {
+ fn clone(&self) -> Self {
+ self.to_owned()
+ }
+}
+
+impl Clone for Box<dyn Source + Send + Sync> {
+ fn clone(&self) -> Self {
+ self.clone_into_box()
+ }
+}
+
+impl Source for Vec<Box<dyn Source + Send + Sync>> {
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new((*self).clone())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ let mut cache: Value = Map::<String, Value>::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<dyn Source + Send + Sync>] {
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new(self.to_owned())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ let mut cache: Value = Map::<String, Value>::new().into();
+
+ for source in self {
+ source.collect_to(&mut cache)?;
+ }
+
+ if let ValueKind::Table(table) = cache.kind {
+ Ok(table)
+ } else {
+ unreachable!();
+ }
+ }
+}
+
+impl<T> Source for Vec<T>
+where
+ T: Source + Sync + Send + Clone + 'static,
+{
+ fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
+ Box::new((*self).clone())
+ }
+
+ fn collect(&self) -> Result<Map<String, Value>> {
+ let mut cache: Value = Map::<String, Value>::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/value.rs b/src/value.rs
new file mode 100644
index 0000000..6ab8ddd
--- /dev/null
+++ b/src/value.rs
@@ -0,0 +1,883 @@
+use std::convert::TryInto;
+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<Value>;
+pub type Table = Map<String, Value>;
+
+impl Default for ValueKind {
+ fn default() -> Self {
+ Self::Nil
+ }
+}
+
+impl<T> From<Option<T>> for ValueKind
+where
+ T: Into<Self>,
+{
+ fn from(value: Option<T>) -> Self {
+ match value {
+ Some(value) => value.into(),
+ None => Self::Nil,
+ }
+ }
+}
+
+impl From<String> 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<i8> for ValueKind {
+ fn from(value: i8) -> Self {
+ Self::I64(value.into())
+ }
+}
+
+impl From<i16> for ValueKind {
+ fn from(value: i16) -> Self {
+ Self::I64(value.into())
+ }
+}
+
+impl From<i32> for ValueKind {
+ fn from(value: i32) -> Self {
+ Self::I64(value.into())
+ }
+}
+
+impl From<i64> for ValueKind {
+ fn from(value: i64) -> Self {
+ Self::I64(value)
+ }
+}
+
+impl From<i128> for ValueKind {
+ fn from(value: i128) -> Self {
+ Self::I128(value)
+ }
+}
+
+impl From<u8> for ValueKind {
+ fn from(value: u8) -> Self {
+ Self::U64(value.into())
+ }
+}
+
+impl From<u16> for ValueKind {
+ fn from(value: u16) -> Self {
+ Self::U64(value.into())
+ }
+}
+
+impl From<u32> for ValueKind {
+ fn from(value: u32) -> Self {
+ Self::U64(value.into())
+ }
+}
+
+impl From<u64> for ValueKind {
+ fn from(value: u64) -> Self {
+ Self::U64(value)
+ }
+}
+
+impl From<u128> for ValueKind {
+ fn from(value: u128) -> Self {
+ Self::U128(value)
+ }
+}
+
+impl From<f64> for ValueKind {
+ fn from(value: f64) -> Self {
+ Self::Float(value)
+ }
+}
+
+impl From<bool> for ValueKind {
+ fn from(value: bool) -> Self {
+ Self::Boolean(value)
+ }
+}
+
+impl<T> From<Map<String, T>> for ValueKind
+where
+ T: Into<Value>,
+{
+ fn from(values: Map<String, T>) -> Self {
+ let t = values.into_iter().map(|(k, v)| (k, v.into())).collect();
+ Self::Table(t)
+ }
+}
+
+impl<T> From<Vec<T>> for ValueKind
+where
+ T: Into<Value>,
+{
+ fn from(values: Vec<T>) -> 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::<String>()
+ }),
+ Self::Array(ref array) => write!(f, "{:?}", {
+ array.iter().map(|e| format!("{}, ", e)).collect::<String>()
+ }),
+ }
+ }
+}
+
+/// 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<String>,
+
+ /// 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<V>(origin: Option<&String>, kind: V) -> Self
+ where
+ V: Into<ValueKind>,
+ {
+ 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> {
+ T::deserialize(self)
+ }
+
+ /// Returns `self` as a bool, if possible.
+ // FIXME: Should this not be `try_into_*` ?
+ pub fn into_bool(self) -> Result<bool> {
+ 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<i64> {
+ match self.kind {
+ ValueKind::I64(value) => Ok(value),
+ ValueKind::I128(value) => value.try_into().map_err(|_| {
+ ConfigError::invalid_type(
+ self.origin,
+ Unexpected::I128(value),
+ "an signed 64 bit or less integer",
+ )
+ }),
+ ValueKind::U64(value) => value.try_into().map_err(|_| {
+ ConfigError::invalid_type(
+ self.origin,
+ Unexpected::U64(value),
+ "an signed 64 bit or less integer",
+ )
+ }),
+ ValueKind::U128(value) => value.try_into().map_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<i128> {
+ match self.kind {
+ ValueKind::I64(value) => Ok(value.into()),
+ ValueKind::I128(value) => Ok(value),
+ ValueKind::U64(value) => Ok(value.into()),
+ ValueKind::U128(value) => value.try_into().map_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<u64> {
+ match self.kind {
+ ValueKind::U64(value) => Ok(value),
+ ValueKind::U128(value) => value.try_into().map_err(|_| {
+ ConfigError::invalid_type(
+ self.origin,
+ Unexpected::U128(value),
+ "an unsigned 64 bit or less integer",
+ )
+ }),
+ ValueKind::I64(value) => value.try_into().map_err(|_| {
+ ConfigError::invalid_type(
+ self.origin,
+ Unexpected::I64(value),
+ "an unsigned 64 bit or less integer",
+ )
+ }),
+ ValueKind::I128(value) => value.try_into().map_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<u128> {
+ match self.kind {
+ ValueKind::U64(value) => Ok(value.into()),
+ ValueKind::U128(value) => Ok(value),
+ ValueKind::I64(value) => value.try_into().map_err(|_| {
+ ConfigError::invalid_type(
+ self.origin,
+ Unexpected::I64(value),
+ "an unsigned 128 bit or less integer",
+ )
+ }),
+ ValueKind::I128(value) => value.try_into().map_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<f64> {
+ 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<String> {
+ 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<Vec<Self>> {
+ 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<Map<String, Self>> {
+ 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<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
+ 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<E>(self, value: bool) -> ::std::result::Result<Value, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_i8<E>(self, value: i8) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_i16<E>(self, value: i16) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_i32<E>(self, value: i32) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Value, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_i128<E>(self, value: i128) -> ::std::result::Result<Value, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_u8<E>(self, value: u8) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_u16<E>(self, value: u16) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_u32<E>(self, value: u32) -> ::std::result::Result<Value, E> {
+ Ok((i64::from(value)).into())
+ }
+
+ #[inline]
+ fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> {
+ // FIXME: This is bad
+ Ok((value as i64).into())
+ }
+
+ #[inline]
+ fn visit_u128<E>(self, value: u128) -> ::std::result::Result<Value, E> {
+ // FIXME: This is bad
+ Ok((value as i128).into())
+ }
+
+ #[inline]
+ fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Value, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_str<E>(self, value: &str) -> ::std::result::Result<Value, E>
+ where
+ E: ::serde::de::Error,
+ {
+ self.visit_string(String::from(value))
+ }
+
+ #[inline]
+ fn visit_string<E>(self, value: String) -> ::std::result::Result<Value, E> {
+ Ok(value.into())
+ }
+
+ #[inline]
+ fn visit_none<E>(self) -> ::std::result::Result<Value, E> {
+ Ok(Value::new(None, ValueKind::Nil))
+ }
+
+ #[inline]
+ fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Value, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Deserialize::deserialize(deserializer)
+ }
+
+ #[inline]
+ fn visit_unit<E>(self) -> ::std::result::Result<Value, E> {
+ Ok(Value::new(None, ValueKind::Nil))
+ }
+
+ #[inline]
+ fn visit_seq<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
+ 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<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
+ 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<T> From<T> for Value
+where
+ T: Into<ValueKind>,
+{
+ 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
new file mode 100644
index 0000000..7e31ec3
--- /dev/null
+++ b/tests/Settings-invalid.hjson
@@ -0,0 +1,4 @@
+{
+ ok: true,
+ error
+}
diff --git a/tests/Settings-invalid.ini b/tests/Settings-invalid.ini
new file mode 100644
index 0000000..f2b8d9b
--- /dev/null
+++ b/tests/Settings-invalid.ini
@@ -0,0 +1,2 @@
+ok : true,
+error
diff --git a/tests/Settings-invalid.json b/tests/Settings-invalid.json
new file mode 100644
index 0000000..ba2d7cb
--- /dev/null
+++ b/tests/Settings-invalid.json
@@ -0,0 +1,4 @@
+{
+ "ok": true,
+ "error"
+}
diff --git a/tests/Settings-invalid.json5 b/tests/Settings-invalid.json5
new file mode 100644
index 0000000..7e97bc1
--- /dev/null
+++ b/tests/Settings-invalid.json5
@@ -0,0 +1,4 @@
+{
+ ok: true
+ error
+}
diff --git a/tests/Settings-invalid.ron b/tests/Settings-invalid.ron
new file mode 100644
index 0000000..0f41c5c
--- /dev/null
+++ b/tests/Settings-invalid.ron
@@ -0,0 +1,4 @@
+(
+ ok: true,
+ error
+)
diff --git a/tests/Settings-invalid.toml b/tests/Settings-invalid.toml
new file mode 100644
index 0000000..4d159a4
--- /dev/null
+++ b/tests/Settings-invalid.toml
@@ -0,0 +1,2 @@
+ok = true
+error = tru
diff --git a/tests/Settings-invalid.yaml b/tests/Settings-invalid.yaml
new file mode 100644
index 0000000..070ff1b
--- /dev/null
+++ b/tests/Settings-invalid.yaml
@@ -0,0 +1,2 @@
+ok: true
+error false
diff --git a/tests/Settings-production.toml b/tests/Settings-production.toml
new file mode 100644
index 0000000..6545b4c
--- /dev/null
+++ b/tests/Settings-production.toml
@@ -0,0 +1,8 @@
+debug = false
+production = true
+
+[place]
+rating = 4.9
+
+[place.creator]
+name = "Somebody New"
diff --git a/tests/Settings.hjson b/tests/Settings.hjson
new file mode 100644
index 0000000..9810e04
--- /dev/null
+++ b/tests/Settings.hjson
@@ -0,0 +1,18 @@
+{
+ 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
new file mode 100644
index 0000000..16badd4
--- /dev/null
+++ b/tests/Settings.ini
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 0000000..babb0ba
--- /dev/null
+++ b/tests/Settings.json
@@ -0,0 +1,19 @@
+{
+ "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
new file mode 100644
index 0000000..4c93e42
--- /dev/null
+++ b/tests/Settings.json5
@@ -0,0 +1,20 @@
+{
+ // 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
new file mode 100644
index 0000000..7882840
--- /dev/null
+++ b/tests/Settings.ron
@@ -0,0 +1,20 @@
+(
+ 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
new file mode 100644
index 0000000..bb53808
--- /dev/null
+++ b/tests/Settings.toml
@@ -0,0 +1,55 @@
+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
new file mode 100644
index 0000000..8e79c29
--- /dev/null
+++ b/tests/Settings.yaml
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000..16badd4
--- /dev/null
+++ b/tests/Settings2.default.ini
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 0000000..bead9d3
--- /dev/null
+++ b/tests/async_builder.rs
@@ -0,0 +1,142 @@
+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<Map<String, Value>, 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::<bool>("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::<String>("place.name").unwrap());
+ assert!(config.get::<bool>("debug_json").unwrap());
+ assert_eq!(1, config.get::<i32>("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::<String>("place.name").unwrap());
+ assert_eq!(1, config.get::<i32>("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::<String>("place.name").unwrap());
+ assert_eq!(1, config.get::<i32>("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::<String>("place.name").unwrap());
+ assert_eq!("blue", config.get::<String>("place.sky").unwrap());
+ assert_eq!(1, config.get::<i32>("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::<String>("place.name").unwrap()
+ );
+ assert_eq!(1, config.get::<i32>("place.number").unwrap());
+}
diff --git a/tests/datetime.rs b/tests/datetime.rs
new file mode 100644
index 0000000..b8c64e5
--- /dev/null
+++ b/tests/datetime.rs
@@ -0,0 +1,110 @@
+#![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<Utc> = s.get("json_datetime").unwrap();
+
+ assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 10, 2, 14, 53).unwrap());
+
+ // TOML
+ let date: DateTime<Utc> = s.get("toml_datetime").unwrap();
+
+ assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 11, 14, 55, 15).unwrap());
+
+ // YAML
+ let date: DateTime<Utc> = s.get("yaml_datetime").unwrap();
+
+ assert_eq!(date, Utc.with_ymd_and_hms(2017, 6, 12, 10, 58, 30).unwrap());
+
+ // INI
+ let date: DateTime<Utc> = s.get("ini_datetime").unwrap();
+
+ assert_eq!(date, Utc.with_ymd_and_hms(2017, 5, 10, 2, 14, 53).unwrap());
+
+ // RON
+ let date: DateTime<Utc> = s.get("ron_datetime").unwrap();
+
+ assert_eq!(date, Utc.with_ymd_and_hms(2021, 4, 19, 11, 33, 2).unwrap());
+}
diff --git a/tests/defaults.rs b/tests/defaults.rs
new file mode 100644
index 0000000..f740259
--- /dev/null
+++ b/tests/defaults.rs
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000..bce4f92
--- /dev/null
+++ b/tests/empty.rs
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 0000000..3a24bde
--- /dev/null
+++ b/tests/env.rs
@@ -0,0 +1,612 @@
+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_uint() {
+ // using a struct in an enum here to make serde use `deserialize_any`
+ #[derive(Deserialize, Debug)]
+ #[serde(tag = "tag")]
+ enum TestUintEnum {
+ Uint(TestUint),
+ }
+
+ #[derive(Deserialize, Debug)]
+ struct TestUint {
+ int_val: u32,
+ }
+
+ temp_env::with_var("INT_VAL", Some("42"), || {
+ let environment = Environment::default().try_parsing(true);
+
+ let config = Config::builder()
+ .set_default("tag", "Uint")
+ .unwrap()
+ .add_source(environment)
+ .build()
+ .unwrap();
+
+ let config: TestUintEnum = config.try_deserialize().unwrap();
+
+ assert!(matches!(
+ config,
+ TestUintEnum::Uint(TestUint { 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!(float_cmp::approx_eq!(f64, 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::<TestIntEnum>().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::<TestFloatEnum>().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::<TestBoolEnum>().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::<TestIntEnum>().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::<TestFloatEnum>().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::<TestBoolEnum>().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<String>,
+ }
+
+ 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<String>,
+ }
+
+ 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);
+ }
+ }
+ })
+}
+
+#[test]
+fn test_parse_int_default() {
+ #[derive(Deserialize, Debug)]
+ struct TestInt {
+ int_val: i32,
+ }
+
+ let environment = Environment::default().try_parsing(true);
+
+ let config = Config::builder()
+ .set_default("int_val", 42_i32)
+ .unwrap()
+ .add_source(environment)
+ .build()
+ .unwrap();
+
+ let config: TestInt = config.try_deserialize().unwrap();
+ assert_eq!(config.int_val, 42);
+}
+
+#[test]
+fn test_parse_uint_default() {
+ #[derive(Deserialize, Debug)]
+ struct TestUint {
+ int_val: u32,
+ }
+
+ let environment = Environment::default().try_parsing(true);
+
+ let config = Config::builder()
+ .set_default("int_val", 42_u32)
+ .unwrap()
+ .add_source(environment)
+ .build()
+ .unwrap();
+
+ let config: TestUint = config.try_deserialize().unwrap();
+ assert_eq!(config.int_val, 42);
+}
diff --git a/tests/errors.rs b/tests/errors.rs
new file mode 100644
index 0000000..773fa46
--- /dev/null
+++ b/tests/errors.rs
@@ -0,0 +1,155 @@
+#![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::<bool>("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::<Value>("boolean_s_parse").unwrap();
+ let res = value.try_deserialize::<bool>();
+
+ 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::<Diode>();
+ 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::<Diode>();
+ 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::<Map<String, Value>>()
+ .into();
+ let confused_d = confused_v.try_deserialize::<Diode>();
+ 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::<Outer>()
+ .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
new file mode 100644
index 0000000..9e4469a
--- /dev/null
+++ b/tests/file.rs
@@ -0,0 +1,70 @@
+#![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
new file mode 100644
index 0000000..4ebf6e3
--- /dev/null
+++ b/tests/file_ini.rs
@@ -0,0 +1,68 @@
+#![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<f32>,
+}
+
+#[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
new file mode 100644
index 0000000..e660997
--- /dev/null
+++ b/tests/file_json.rs
@@ -0,0 +1,113 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..a1cb733
--- /dev/null
+++ b/tests/file_json5.rs
@@ -0,0 +1,91 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..64e2cae
--- /dev/null
+++ b/tests/file_ron.rs
@@ -0,0 +1,91 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..9eed788
--- /dev/null
+++ b/tests/file_toml.rs
@@ -0,0 +1,103 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize, PartialEq)]
+struct PlaceNumber(u8);
+
+#[derive(Debug, Deserialize, PartialEq)]
+struct AsciiCode(i8);
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ code: AsciiCode,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+#[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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..233b92c
--- /dev/null
+++ b/tests/file_yaml.rs
@@ -0,0 +1,93 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..fe66d34
--- /dev/null
+++ b/tests/get.rs
@@ -0,0 +1,280 @@
+#![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<String>,
+ reviews: u64,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ 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::<bool>("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<String, Value> = 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<String, String> = c.get("place.creator").unwrap();
+
+ if cfg!(feature = "preserve_order") {
+ assert_eq!(
+ m.into_iter().collect::<Vec<(String, String)>>(),
+ 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<String, Value>,
+ }
+
+ 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<i64> = 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<String>,
+ }
+
+ 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<String, Diode>,
+ }
+
+ 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<Quark, usize>,
+ // Just to make sure that set keys work too.
+ quarks: HashSet<Quark>,
+ }
+
+ 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<u32, u32>,
+ }
+
+ 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
new file mode 100644
index 0000000..7777ef2
--- /dev/null
+++ b/tests/integer_range.rs
@@ -0,0 +1,52 @@
+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);
+}
+
+#[test]
+#[should_panic]
+fn invalid_signedness() {
+ let c = Config::builder()
+ .add_source(config::File::from_str(
+ r#"
+ [settings]
+ port = -1
+ "#,
+ config::FileFormat::Toml,
+ ))
+ .build()
+ .unwrap();
+
+ let _: u32 = c.get("settings.port").unwrap();
+}
diff --git a/tests/legacy/datetime.rs b/tests/legacy/datetime.rs
new file mode 100644
index 0000000..7e63e68
--- /dev/null
+++ b/tests/legacy/datetime.rs
@@ -0,0 +1,134 @@
+#![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<Utc> = s.get("json_datetime").unwrap();
+
+ assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53));
+
+ // TOML
+ let date: DateTime<Utc> = s.get("toml_datetime").unwrap();
+
+ assert_eq!(date, Utc.ymd(2017, 5, 11).and_hms(14, 55, 15));
+
+ // YAML
+ let date: DateTime<Utc> = s.get("yaml_datetime").unwrap();
+
+ assert_eq!(date, Utc.ymd(2017, 6, 12).and_hms(10, 58, 30));
+
+ // HJSON
+ let date: DateTime<Utc> = s.get("hjson_datetime").unwrap();
+
+ assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53));
+
+ // INI
+ let date: DateTime<Utc> = s.get("ini_datetime").unwrap();
+
+ assert_eq!(date, Utc.ymd(2017, 5, 10).and_hms(2, 14, 53));
+
+ // RON
+ let date: DateTime<Utc> = 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
new file mode 100644
index 0000000..cde1482
--- /dev/null
+++ b/tests/legacy/env.rs
@@ -0,0 +1,348 @@
+
+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::<TestIntEnum>().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::<TestFloatEnum>().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::<TestBoolEnum>().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::<TestIntEnum>().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::<TestFloatEnum>().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::<TestBoolEnum>().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
new file mode 100644
index 0000000..885580f
--- /dev/null
+++ b/tests/legacy/errors.rs
@@ -0,0 +1,134 @@
+#![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::<bool>("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::<Value>("boolean_s_parse").unwrap();
+ let res = value.try_deserialize::<bool>();
+
+ 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::<Diode>();
+ 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::<Diode>();
+ 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::<Map<String, Value>>()
+ .into();
+ let confused_d = confused_v.try_deserialize::<Diode>();
+ 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::<Outer>().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
new file mode 100644
index 0000000..5900646
--- /dev/null
+++ b/tests/legacy/file.rs
@@ -0,0 +1,54 @@
+#![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
new file mode 100644
index 0000000..5cbf63d
--- /dev/null
+++ b/tests/legacy/file_ini.rs
@@ -0,0 +1,66 @@
+#![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<f32>,
+}
+
+#[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
new file mode 100644
index 0000000..72fd5eb
--- /dev/null
+++ b/tests/legacy/file_json.rs
@@ -0,0 +1,113 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..7f31c0e
--- /dev/null
+++ b/tests/legacy/file_ron.rs
@@ -0,0 +1,90 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..795f1ab
--- /dev/null
+++ b/tests/legacy/file_toml.rs
@@ -0,0 +1,102 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize, PartialEq)]
+struct PlaceNumber(u8);
+
+#[derive(Debug, Deserialize, PartialEq)]
+struct AsciiCode(i8);
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ code: AsciiCode,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+#[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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..21d4138
--- /dev/null
+++ b/tests/legacy/file_yaml.rs
@@ -0,0 +1,93 @@
+#![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<String>,
+ reviews: u64,
+ creator: Map<String, Value>,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ place: Place,
+ #[serde(rename = "arr")]
+ elements: Vec<String>,
+}
+
+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<(String, config::Value)>>(),
+ 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
new file mode 100644
index 0000000..24f0d35
--- /dev/null
+++ b/tests/legacy/get.rs
@@ -0,0 +1,280 @@
+#![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<String>,
+ reviews: u64,
+ rating: Option<f32>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Settings {
+ debug: f64,
+ production: Option<String>,
+ 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::<bool>("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<String, Value> = 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<String, String> = c.get("place.creator").unwrap();
+
+ if cfg!(feature = "preserve_order") {
+ assert_eq!(
+ m.into_iter().collect::<Vec<(String, String)>>(),
+ 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<String, Value>,
+ }
+
+ 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<i64> = 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<String>,
+ }
+
+ 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<String, Diode>,
+ }
+
+ 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<Quark, usize>,
+ // Just to make sure that set keys work too.
+ quarks: HashSet<Quark>,
+ }
+
+ 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<u32, u32>,
+ }
+
+ 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
new file mode 100644
index 0000000..74463d6
--- /dev/null
+++ b/tests/legacy/merge.rs
@@ -0,0 +1,60 @@
+#![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<String, String> = c.get("place.creator").unwrap();
+ assert_eq!(
+ m.into_iter().collect::<Vec<(String, String)>>(),
+ 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
new file mode 100644
index 0000000..cd0a16a
--- /dev/null
+++ b/tests/legacy/mod.rs
@@ -0,0 +1,11 @@
+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
new file mode 100644
index 0000000..169e462
--- /dev/null
+++ b/tests/legacy/set.rs
@@ -0,0 +1,91 @@
+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
new file mode 100644
index 0000000..ce28838
--- /dev/null
+++ b/tests/legacy_tests.rs
@@ -0,0 +1,2 @@
+#[allow(deprecated)]
+pub mod legacy;
diff --git a/tests/merge.rs b/tests/merge.rs
new file mode 100644
index 0000000..0469064
--- /dev/null
+++ b/tests/merge.rs
@@ -0,0 +1,57 @@
+#![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<String, String> = c.get("place.creator").unwrap();
+ assert_eq!(
+ m.into_iter().collect::<Vec<(String, String)>>(),
+ 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
new file mode 100644
index 0000000..59b5b84
--- /dev/null
+++ b/tests/set.rs
@@ -0,0 +1,91 @@
+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
new file mode 100644
index 0000000..4ad173a
--- /dev/null
+++ b/tests/types/i64.toml
@@ -0,0 +1 @@
+value = 120
diff --git a/tests/weird_keys.rs b/tests/weird_keys.rs
new file mode 100644
index 0000000..c997fe0
--- /dev/null
+++ b/tests/weird_keys.rs
@@ -0,0 +1,111 @@
+// 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::<SettingsColon>(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::<SettingsColon>(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::<SettingsSlash>(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::<SettingsSlash>(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::<SettingsDoubleBackslash>(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::<SettingsDoubleBackslash>(config, FileFormat::Json);
+ assert_eq!(cfg.foo, 8);
+ assert_eq!(cfg.bar, 12);
+}