diff options
author | Jeongik Cha <jeongik@google.com> | 2023-09-27 10:19:08 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-09-27 10:19:08 +0000 |
commit | 576d980bd425fb34497c8908392d54167ee21841 (patch) | |
tree | 1d8ee82a8ceb5276c1ae961bdb7c5b710966f685 | |
parent | 919c1e4e74e1a5907ba6e6d75e4af0821df1e725 (diff) | |
parent | 886b4178e770cc971d1995b5bf62fb941d640bdd (diff) | |
download | config-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>
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" @@ -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 @@ -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); +} |