├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── examples
├── optional.rs
└── prefixed.rs
├── rustfmt.toml
└── src
├── error.rs
└── lib.rs
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report 🐛
3 | about: Did something not work as expected?
4 | ---
5 |
6 |
7 |
8 | ## 🐛 Bug description
9 | Describe your issue in detail.
10 |
11 | #### 🤔 Expected Behavior
12 |
13 |
14 | #### 👟 Steps to reproduce
15 |
16 |
17 | #### 🌍 Your environment
18 |
19 |
20 | envy version:
21 |
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request 💡
3 | about: Suggest a new idea for envy
4 | ---
5 |
6 |
7 |
8 | ## 💡 Feature description
9 |
10 |
11 | #### 💻 Basic example
12 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ## What did you implement:
8 |
9 |
12 |
13 | Closes: #xxx
14 |
15 | #### How did you verify your change:
16 |
17 | #### What (if anything) would need to be called out in the CHANGELOG for the next release:
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: cargo
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Main
3 |
4 | on:
5 | workflow_dispatch:
6 | push:
7 | paths-ignore:
8 | - '*.md'
9 | branches:
10 | - main
11 | - master
12 | tags:
13 | - '**'
14 | pull_request:
15 | paths-ignore:
16 | - '*.md'
17 | branches:
18 | - main
19 | - master
20 |
21 | env:
22 | CARGO_TERM_COLOR: always
23 |
24 | jobs:
25 | codestyle:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Set up Rust
29 | uses: hecrj/setup-rust-action@v1
30 | with:
31 | components: rustfmt
32 | rust-version: nightly
33 | - uses: actions/checkout@v2
34 | - run: cargo fmt --all -- --check
35 |
36 | lint:
37 | runs-on: ubuntu-latest
38 | steps:
39 | - name: Set up Rust
40 | uses: hecrj/setup-rust-action@v1
41 | with:
42 | components: clippy
43 | - uses: actions/checkout@v2
44 | - run: cargo clippy --all-targets -- -D clippy::all
45 |
46 | compile:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - name: Set up Rust
50 | uses: hecrj/setup-rust-action@v1
51 | - uses: actions/checkout@master
52 | - run: cargo check --all
53 |
54 | test:
55 | needs: [codestyle, lint, compile]
56 | strategy:
57 | matrix:
58 | rust: [stable, beta, nightly]
59 | runs-on: ubuntu-latest
60 | steps:
61 | - name: Setup Rust
62 | uses: hecrj/setup-rust-action@v1
63 | with:
64 | rust-version: ${{ matrix.rust }}
65 | - name: Checkout
66 | uses: actions/checkout@v2
67 | - name: Test
68 | run: cargo test
69 | - name: Coverage
70 | if: matrix.rust == 'stable'
71 | run: |
72 | # tarpaulin knows how to extract data from ci
73 | # ci services and GitHub actions is not one of them
74 | # work around that by masquerading as travis
75 | # https://github.com/xd009642/coveralls-api/blob/6da4ccd7c6eaf1df04cfd1e560362de70fa80605/src/lib.rs#L247-L262
76 | export TRAVIS_JOB_ID=${GITHUB_SHA}
77 | export TRAVIS_PULL_REQUEST=false
78 | export TRAVIS_BRANCH=${GITHUB_REF##*/}
79 | cargo install cargo-tarpaulin
80 | cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID
81 |
82 | publish-docs:
83 | if: github.ref == 'refs/heads/master'
84 | runs-on: ubuntu-latest
85 | needs: [test]
86 | steps:
87 | - name: Set up Rust
88 | uses: hecrj/setup-rust-action@v1
89 | - uses: actions/checkout@v2
90 | - name: Generate Docs
91 | shell: bash
92 | run: |
93 | cargo doc --no-deps
94 | echo "" > target/doc/index.html
95 | - name: Publish
96 | uses: peaceiris/actions-gh-pages@v3
97 | with:
98 | github_token: ${{ secrets.GITHUB_TOKEN }}
99 | publish_dir: ./target/doc
100 |
101 | publish-crate:
102 | runs-on: ubuntu-latest
103 | if: startsWith(github.ref, 'refs/tags/')
104 | needs: [test]
105 | steps:
106 | - name: Set up Rust
107 | uses: hecrj/setup-rust-action@v1
108 | - uses: actions/checkout@v2
109 | - name: Publish
110 | shell: bash
111 | run: cargo publish --token ${{ secrets.CRATES_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bk
2 | target
3 | Cargo.lock
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.4.2
2 |
3 | * Correctly deserialize empty strings into empty sequence [#51](https://github.com/softprops/envy/pull/51)
4 |
5 | # 0.4.1
6 |
7 | * Add support for unit-variant enums as values, without using the `#[serde(field_identifier)]` attribute [#46](https://github.com/softprops/envy/pull/46)
8 |
9 | # 0.4.0
10 |
11 | * include field name and provided value in error messages [#28](https://github.com/softprops/envy/pull/28) [#36](https://github.com/softprops/envy/pull/36)
12 | * add support for new type struct types in fields [#32](https://github.com/softprops/envy/pull/32)
13 | * fix warnings with now deprecated `trim_left_matches` [#34](https://github.com/softprops/envy/pull/34)
14 | * switch to 2018 edition rust [#37](https://github.com/softprops/envy/pull/37)
15 |
16 | # 0.3.3
17 |
18 | * update `from_iter(..)` to accept `std::iter::IntoIterator` types
19 |
20 | This is a backwards compatible change because all Iterators have a [provided impl for IntoIterator](https://doc.rust-lang.org/src/core/iter/traits.rs.html#255-262) by default.
21 |
22 | # 0.3.2
23 |
24 | * add new `envy::prefixed(...)` interface for prefixed env var names
25 |
26 | # 0.3.1
27 |
28 | * fix option support
29 |
30 | # 0.3.0
31 |
32 | * upgrade to the latest serde (1.0)
33 |
34 | # 0.2.0
35 |
36 | * upgrade to the latest serde (0.9)
37 |
38 | # 0.1.2
39 |
40 | * upgrade to latest serde (0.8)
41 |
42 | # 0.1.1 (2016-07-10)
43 |
44 | * allow for customization via built in serde [field annotations](https://github.com/serde-rs/serde#annotations)
45 |
46 | # 0.1.0 (2016-07-02)
47 |
48 | * initial release
49 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Filing an Issue
4 |
5 | If you are trying to use `envy` and run into an issue- please file an
6 | issue! We'd love to get you up and running, even if the issue you have might
7 | not be directly related to the code in `envy`. This library seeks to make
8 | it easy for developers to get going, so there's a good chance we can do
9 | something to alleviate the issue by making `envy` better documented or
10 | more robust to different developer environments.
11 |
12 | When filing an issue, do your best to be as specific as possible
13 | The faster was can reproduce your issue, the faster we
14 | can fix it for you!
15 |
16 | ## Submitting a PR
17 |
18 | If you are considering filing a pull request, make sure that there's an issue
19 | filed for the work you'd like to do. There might be some discussion required!
20 | Filing an issue first will help ensure that the work you put into your pull
21 | request will get merged :)
22 |
23 | Before you submit your pull request, check that you have completed all of the
24 | steps mentioned in the pull request template. Link the issue that your pull
25 | request is responding to, and format your code using [rustfmt][rustfmt].
26 |
27 | ### Configuring rustfmt
28 |
29 | Before submitting code in a PR, make sure that you have formatted the codebase
30 | using [rustfmt][rustfmt]. `rustfmt` is a tool for formatting Rust code, which
31 | helps keep style consistent across the project. If you have not used `rustfmt`
32 | before, it is not too difficult.
33 |
34 | If you have not already configured `rustfmt` for the
35 | nightly toolchain, it can be done using the following steps:
36 |
37 | **1. Use Nightly Toolchain**
38 |
39 | Install the nightly toolchain. This will only be necessary as long as rustfmt produces different results on stable and nightly.
40 |
41 | ```sh
42 | $ rustup toolchain install nightly
43 | ```
44 |
45 | **2. Add the rustfmt component**
46 |
47 | Install the most recent version of `rustfmt` using this command:
48 |
49 | ```sh
50 | $ rustup component add rustfmt-preview --toolchain nightly
51 | ```
52 |
53 | **3. Running rustfmt**
54 |
55 | To run `rustfmt`, use this command:
56 |
57 | ```sh
58 | cargo +nightly fmt
59 | ```
60 |
61 | [rustfmt]: https://github.com/rust-lang-nursery/rustfmt
62 |
63 | ### IDE Configuration files
64 | Machine specific configuration files may be generated by your IDE while working on the project. Please make sure to add these files to a global .gitignore so they are kept from accidentally being committed to the project and causing issues for other contributors.
65 |
66 | Some examples of these files are the `.idea` folder created by JetBrains products (WebStorm, IntelliJ, etc) as well as `.vscode` created by Visual Studio Code for workspace specific settings.
67 |
68 | For help setting up a global .gitignore check out this [GitHub article]!
69 |
70 | [GitHub article]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
71 |
72 | ## Conduct
73 |
74 | This project follows the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html)
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "envy"
3 | version = "0.4.2"
4 | authors = ["softprops "]
5 | description = "deserialize env vars into typesafe structs"
6 | documentation = "https://softprops.github.io/envy"
7 | homepage = "https://github.com/softprops/envy"
8 | repository = "https://github.com/softprops/envy"
9 | keywords = ["serde", "env"]
10 | license = "MIT"
11 | readme = "README.md"
12 | edition = "2021"
13 | categories = [
14 | "config"
15 | ]
16 |
17 | [badges]
18 | coveralls = { repository = "softprops/envy" }
19 | travis-ci = { repository = "softprops/envy" }
20 |
21 | [dependencies]
22 | serde = "1.0"
23 |
24 | [dev-dependencies]
25 | serde = { version = "1.0", features = ["derive"] }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016-2024 Doug Tangren
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | test-coverage:
3 | @docker run -it --rm \
4 | --security-opt seccomp=unconfined \
5 | -v "$(PWD):/volume" \
6 | xd009642/tarpaulin
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # envy [](https://github.com/softprops/envy/actions) [](https://coveralls.io/github/softprops/envy?branch=master) [](LICENSE) [](https://crates.io/crates/envy) [](https://softprops.github.io/envy)
2 |
3 | > deserialize environment variables into typesafe structs
4 |
5 | ## 📦 install
6 |
7 | Run `cargo add envy` or add the following to your `Cargo.toml` file.
8 |
9 | ```toml
10 | [dependencies]
11 | envy = "0.4"
12 | ```
13 |
14 | ## 🤸 usage
15 |
16 | A typical envy usage looks like the following. Assuming your rust program looks something like this...
17 |
18 | > 💡 These examples use Serde's [derive feature](https://serde.rs/derive.html)
19 |
20 | ```rust
21 | use serde::Deserialize;
22 |
23 | #[derive(Deserialize, Debug)]
24 | struct Config {
25 | foo: u16,
26 | bar: bool,
27 | baz: String,
28 | boom: Option
29 | }
30 |
31 | fn main() {
32 | match envy::from_env::() {
33 | Ok(config) => println!("{:#?}", config),
34 | Err(error) => panic!("{:#?}", error)
35 | }
36 | }
37 | ```
38 |
39 | ... export some environment variables
40 |
41 | ```bash
42 | $ FOO=8080 BAR=true BAZ=hello yourapp
43 | ```
44 |
45 | You should be able to access a completely typesafe config struct deserialized from env vars.
46 |
47 | Envy assumes an env var exists for each struct field with a matching name in all uppercase letters. i.e. A struct field `foo_bar` would map to an env var named `FOO_BAR`.
48 |
49 | Structs with `Option` type fields will successfully be deserialized when their associated env var is absent.
50 |
51 | Envy also supports deserializing `Vecs` from comma separated env var values.
52 |
53 | Because envy is built on top of serde, you can use all of serde's [attributes](https://serde.rs/attributes.html) to your advantage.
54 |
55 | For instance let's say your app requires a field but would like a sensible default when one is not provided.
56 | ```rust
57 |
58 | /// provides default value for zoom if ZOOM env var is not set
59 | fn default_zoom() -> u16 {
60 | 32
61 | }
62 |
63 | #[derive(Deserialize, Debug)]
64 | struct Config {
65 | foo: u16,
66 | bar: bool,
67 | baz: String,
68 | boom: Option,
69 | #[serde(default="default_zoom")]
70 | zoom: u16
71 | }
72 | ```
73 |
74 | The following will yield an application configured with a zoom of 32
75 |
76 | ```bash
77 | $ FOO=8080 BAR=true BAZ=hello yourapp
78 | ```
79 |
80 | The following will yield an application configured with a zoom of 10
81 |
82 | ```bash
83 | $ FOO=8080 BAR=true BAZ=hello ZOOM=10 yourapp
84 | ```
85 |
86 | The common pattern for prefixing env var names for a specific app is supported using
87 | the `envy::prefixed(prefix)` interface. Asumming your env vars are prefixed with `APP_`
88 | the above example may instead look like
89 |
90 | ```rust
91 | use serde::Deserialize;
92 |
93 | #[derive(Deserialize, Debug)]
94 | struct Config {
95 | foo: u16,
96 | bar: bool,
97 | baz: String,
98 | boom: Option
99 | }
100 |
101 | fn main() {
102 | match envy::prefixed("APP_").from_env::() {
103 | Ok(config) => println!("{:#?}", config),
104 | Err(error) => panic!("{:#?}", error)
105 | }
106 | }
107 | ```
108 |
109 | the expectation would then be to export the same environment variables prefixed with `APP_`
110 |
111 | ```bash
112 | $ APP_FOO=8080 APP_BAR=true APP_BAZ=hello yourapp
113 | ```
114 |
115 | > 👭 Consider this crate a cousin of [envy-store](https://github.com/softprops/envy-store), a crate for deserializing AWS parameter store values into typesafe structs and [recap](https://github.com/softprops/recap), a crate for deserializing named regex capture groups into typesafe structs.
116 |
117 | Doug Tangren (softprops) 2016-2024
118 |
--------------------------------------------------------------------------------
/examples/optional.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Deserialize)]
4 | struct Config {
5 | size: Option,
6 | }
7 |
8 | fn main() {
9 | match envy::from_env::() {
10 | Ok(config) => println!("provided config.size {:?}", config.size),
11 | Err(err) => println!("error parsing config from env: {}", err),
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/prefixed.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Deserialize)]
4 | struct Config {
5 | bar: Option,
6 | }
7 |
8 | fn main() {
9 | match envy::prefixed("FOO_").from_env::() {
10 | Ok(config) => println!("provided config.bar {:?}", config.bar),
11 | Err(err) => println!("error parsing config from env: {}", err),
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_params_layout
2 | fn_params_layout = "Vertical"
3 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#imports_granularity
4 | imports_granularity = "Crate"
5 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#format_code_in_doc_comments
6 | format_code_in_doc_comments = true
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | //! Error types
2 | use serde::de::Error as SerdeError;
3 | use std::{error::Error as StdError, fmt};
4 |
5 | /// Types of errors that may result from failed attempts
6 | /// to deserialize a type from env vars
7 | #[derive(Debug, Clone, PartialEq)]
8 | pub enum Error {
9 | MissingValue(String),
10 | Custom(String),
11 | }
12 |
13 | impl StdError for Error {}
14 |
15 | impl fmt::Display for Error {
16 | fn fmt(
17 | &self,
18 | fmt: &mut fmt::Formatter,
19 | ) -> fmt::Result {
20 | match self {
21 | Error::MissingValue(field) => write!(fmt, "missing value for {}", &field),
22 | Error::Custom(ref msg) => write!(fmt, "{}", msg),
23 | }
24 | }
25 | }
26 |
27 | impl SerdeError for Error {
28 | fn custom(msg: T) -> Self {
29 | Error::Custom(format!("{}", msg))
30 | }
31 |
32 | fn missing_field(field: &'static str) -> Error {
33 | Error::MissingValue(field.into())
34 | }
35 | }
36 |
37 | #[cfg(test)]
38 | mod tests {
39 | use super::*;
40 |
41 | fn impl_std_error(_: E) {}
42 |
43 | #[test]
44 | fn error_impl_std_error() {
45 | impl_std_error(Error::MissingValue("FOO_BAR".into()));
46 | impl_std_error(Error::Custom("whoops".into()))
47 | }
48 |
49 | #[test]
50 | fn error_display() {
51 | assert_eq!(
52 | format!("{}", Error::MissingValue("FOO_BAR".into())),
53 | "missing value for FOO_BAR"
54 | );
55 |
56 | assert_eq!(format!("{}", Error::Custom("whoops".into())), "whoops")
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Envy is a library for deserializing environment variables into typesafe structs
2 | //!
3 | //! # Examples
4 | //!
5 | //! A typical usecase for envy is deserializing configuration store in an process' environment into a struct
6 | //! whose fields map to the names of env vars.
7 | //!
8 | //! Serde makes it easy to provide a deserializable struct with its [deriveable Deserialize](https://serde.rs/derive.html)
9 | //! procedural macro.
10 | //!
11 | //! Simply ask for an instance of that struct from envy's `from_env` function.
12 | //!
13 | //! ```no_run
14 | //! use serde::Deserialize;
15 | //!
16 | //! #[derive(Deserialize, Debug)]
17 | //! struct Config {
18 | //! foo: u16,
19 | //! bar: bool,
20 | //! baz: String,
21 | //! boom: Option,
22 | //! }
23 | //!
24 | //! match envy::from_env::() {
25 | //! Ok(config) => println!("{:#?}", config),
26 | //! Err(error) => eprintln!("{:#?}", error),
27 | //! }
28 | //! ```
29 | //!
30 | //! Special treatment is given to collections. For config fields that store a `Vec` of values,
31 | //! use an env var that uses a comma separated value.
32 | //!
33 | //! All serde modifiers should work as is.
34 | //!
35 | //! Enums with unit variants can be used as values:
36 | //!
37 | //! ```no_run
38 | //! # use serde::Deserialize;
39 | //!
40 | //! #[derive(Deserialize, Debug, PartialEq)]
41 | //! #[serde(rename_all = "lowercase")]
42 | //! pub enum Size {
43 | //! Small,
44 | //! Medium,
45 | //! Large,
46 | //! }
47 | //!
48 | //! #[derive(Deserialize, Debug)]
49 | //! struct Config {
50 | //! size: Size,
51 | //! }
52 | //!
53 | //! // set env var for size as `SIZE=medium`
54 | //! match envy::from_env::() {
55 | //! Ok(config) => println!("{:#?}", config),
56 | //! Err(error) => eprintln!("{:#?}", error),
57 | //! }
58 | //! ```
59 |
60 | use serde::de::{
61 | self,
62 | value::{MapDeserializer, SeqDeserializer},
63 | IntoDeserializer,
64 | };
65 | use std::{
66 | borrow::Cow,
67 | env,
68 | iter::{empty, IntoIterator},
69 | };
70 |
71 | // Ours
72 | mod error;
73 | pub use crate::error::Error;
74 |
75 | /// A type result type specific to `envy::Errors`
76 | pub type Result = std::result::Result;
77 |
78 | #[derive(Default)]
79 | struct VarsOptions {
80 | keep_names: bool,
81 | }
82 |
83 | struct Vars
84 | where
85 | Iter: IntoIterator- ,
86 | {
87 | inner: Iter,
88 | options: VarsOptions,
89 | }
90 |
91 | struct Val(String, String);
92 |
93 | impl<'de> IntoDeserializer<'de, Error> for Val {
94 | type Deserializer = Self;
95 |
96 | fn into_deserializer(self) -> Self::Deserializer {
97 | self
98 | }
99 | }
100 |
101 | struct VarName(String);
102 |
103 | impl<'de> IntoDeserializer<'de, Error> for VarName {
104 | type Deserializer = Self;
105 |
106 | fn into_deserializer(self) -> Self::Deserializer {
107 | self
108 | }
109 | }
110 |
111 | impl> Iterator for Vars {
112 | type Item = (VarName, Val);
113 |
114 | fn next(&mut self) -> Option {
115 | self.inner.next().map(|(k, v)| {
116 | let var_name = if self.options.keep_names {
117 | k.clone()
118 | } else {
119 | k.to_lowercase()
120 | };
121 | (VarName(var_name), Val(k, v))
122 | })
123 | }
124 | }
125 |
126 | macro_rules! forward_parsed_values {
127 | ($($ty:ident => $method:ident,)*) => {
128 | $(
129 | fn $method(self, visitor: V) -> Result
130 | where V: de::Visitor<'de>
131 | {
132 | match self.1.parse::<$ty>() {
133 | Ok(val) => val.into_deserializer().$method(visitor),
134 | Err(e) => Err(de::Error::custom(format_args!("{} while parsing value '{}' provided by {}", e, self.1, self.0)))
135 | }
136 | }
137 | )*
138 | }
139 | }
140 |
141 | impl<'de> de::Deserializer<'de> for Val {
142 | type Error = Error;
143 | fn deserialize_any(
144 | self,
145 | visitor: V,
146 | ) -> Result
147 | where
148 | V: de::Visitor<'de>,
149 | {
150 | self.1.into_deserializer().deserialize_any(visitor)
151 | }
152 |
153 | fn deserialize_seq(
154 | self,
155 | visitor: V,
156 | ) -> Result
157 | where
158 | V: de::Visitor<'de>,
159 | {
160 | // std::str::split doesn't work as expected for our use case: when we
161 | // get an empty string we want to produce an empty Vec, but split would
162 | // still yield an iterator with an empty string in it. So we need to
163 | // special case empty strings.
164 | if self.1.is_empty() {
165 | SeqDeserializer::new(empty::()).deserialize_seq(visitor)
166 | } else {
167 | let values = self
168 | .1
169 | .split(',')
170 | .map(|v| Val(self.0.clone(), v.trim().to_owned()));
171 | SeqDeserializer::new(values).deserialize_seq(visitor)
172 | }
173 | }
174 |
175 | fn deserialize_option(
176 | self,
177 | visitor: V,
178 | ) -> Result
179 | where
180 | V: de::Visitor<'de>,
181 | {
182 | visitor.visit_some(self)
183 | }
184 |
185 | forward_parsed_values! {
186 | bool => deserialize_bool,
187 | u8 => deserialize_u8,
188 | u16 => deserialize_u16,
189 | u32 => deserialize_u32,
190 | u64 => deserialize_u64,
191 | i8 => deserialize_i8,
192 | i16 => deserialize_i16,
193 | i32 => deserialize_i32,
194 | i64 => deserialize_i64,
195 | f32 => deserialize_f32,
196 | f64 => deserialize_f64,
197 | }
198 |
199 | #[inline]
200 | fn deserialize_newtype_struct(
201 | self,
202 | _: &'static str,
203 | visitor: V,
204 | ) -> Result
205 | where
206 | V: serde::de::Visitor<'de>,
207 | {
208 | visitor.visit_newtype_struct(self)
209 | }
210 |
211 | fn deserialize_enum(
212 | self,
213 | _name: &'static str,
214 | _variants: &'static [&'static str],
215 | visitor: V,
216 | ) -> Result
217 | where
218 | V: de::Visitor<'de>,
219 | {
220 | visitor.visit_enum(self.1.into_deserializer())
221 | }
222 |
223 | serde::forward_to_deserialize_any! {
224 | char str string unit
225 | bytes byte_buf map unit_struct tuple_struct
226 | identifier tuple ignored_any
227 | struct
228 | }
229 | }
230 |
231 | impl<'de> de::Deserializer<'de> for VarName {
232 | type Error = Error;
233 | fn deserialize_any(
234 | self,
235 | visitor: V,
236 | ) -> Result
237 | where
238 | V: de::Visitor<'de>,
239 | {
240 | self.0.into_deserializer().deserialize_any(visitor)
241 | }
242 |
243 | #[inline]
244 | fn deserialize_newtype_struct(
245 | self,
246 | _: &'static str,
247 | visitor: V,
248 | ) -> Result
249 | where
250 | V: serde::de::Visitor<'de>,
251 | {
252 | visitor.visit_newtype_struct(self)
253 | }
254 |
255 | serde::forward_to_deserialize_any! {
256 | char str string unit seq option
257 | bytes byte_buf map unit_struct tuple_struct
258 | identifier tuple ignored_any enum
259 | struct bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64
260 | }
261 | }
262 |
263 | /// A deserializer for env vars
264 | struct Deserializer<'de, Iter: Iterator
- > {
265 | inner: MapDeserializer<'de, Vars, Error>,
266 | }
267 |
268 | impl<'de, Iter: Iterator
- > Deserializer<'de, Iter> {
269 | fn new(
270 | vars: Iter,
271 | options: Option,
272 | ) -> Self {
273 | Deserializer {
274 | inner: MapDeserializer::new(Vars {
275 | inner: vars,
276 | options: options.unwrap_or_default(),
277 | }),
278 | }
279 | }
280 | }
281 |
282 | impl<'de, Iter: Iterator
- > de::Deserializer<'de>
283 | for Deserializer<'de, Iter>
284 | {
285 | type Error = Error;
286 | fn deserialize_any(
287 | self,
288 | visitor: V,
289 | ) -> Result
290 | where
291 | V: de::Visitor<'de>,
292 | {
293 | self.deserialize_map(visitor)
294 | }
295 |
296 | fn deserialize_map(
297 | self,
298 | visitor: V,
299 | ) -> Result
300 | where
301 | V: de::Visitor<'de>,
302 | {
303 | visitor.visit_map(self.inner)
304 | }
305 |
306 | serde::forward_to_deserialize_any! {
307 | bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq
308 | bytes byte_buf unit_struct tuple_struct
309 | identifier tuple ignored_any option newtype_struct enum
310 | struct
311 | }
312 | }
313 |
314 | /// Deserializes a type based on information stored in env variables
315 | pub fn from_env() -> Result
316 | where
317 | T: de::DeserializeOwned,
318 | {
319 | from_iter(env::vars())
320 | }
321 |
322 | /// Deserializes a type based on an iterable of `(String, String)`
323 | /// representing keys and values
324 | pub fn from_iter(iter: Iter) -> Result
325 | where
326 | T: de::DeserializeOwned,
327 | Iter: IntoIterator
- ,
328 | {
329 | T::deserialize(Deserializer::new(iter.into_iter(), None)).map_err(|error| match error {
330 | Error::MissingValue(value) => Error::MissingValue(value.to_uppercase()),
331 | _ => error,
332 | })
333 | }
334 |
335 | /// A type which filters env vars with a prefix for use as serde field inputs.
336 | ///
337 | /// These types are created with the [prefixed](fn.prefixed.html) module function.
338 | pub struct Prefixed<'a>(Cow<'a, str>);
339 |
340 | impl<'a> Prefixed<'a> {
341 | /// Deserializes a type based on prefixed env variables
342 | pub fn from_env(&self) -> Result
343 | where
344 | T: de::DeserializeOwned,
345 | {
346 | self.from_iter(env::vars())
347 | }
348 |
349 | /// Deserializes a type based on prefixed (String, String) tuples
350 | pub fn from_iter(
351 | &self,
352 | iter: Iter,
353 | ) -> Result
354 | where
355 | T: de::DeserializeOwned,
356 | Iter: IntoIterator
- ,
357 | {
358 | crate::from_iter(iter.into_iter().filter_map(|(k, v)| {
359 | if k.starts_with(self.0.as_ref()) {
360 | Some((k.trim_start_matches(self.0.as_ref()).to_owned(), v))
361 | } else {
362 | None
363 | }
364 | }))
365 | .map_err(|error| match error {
366 | Error::MissingValue(value) => Error::MissingValue(
367 | format!("{prefix}{value}", prefix = self.0, value = value).to_uppercase(),
368 | ),
369 | _ => error,
370 | })
371 | }
372 | }
373 |
374 | /// Produces a instance of `Prefixed` for prefixing env variable names
375 | ///
376 | /// # Example
377 | ///
378 | /// ```no_run
379 | /// use serde::Deserialize;
380 | ///
381 | /// #[derive(Deserialize, Debug)]
382 | /// struct Config {
383 | /// foo: u16,
384 | /// bar: bool,
385 | /// baz: String,
386 | /// boom: Option,
387 | /// }
388 | ///
389 | /// // all env variables will be expected to be prefixed with APP_
390 | /// // i.e. APP_FOO, APP_BAR, etc
391 | /// match envy::prefixed("APP_").from_env::() {
392 | /// Ok(config) => println!("{:#?}", config),
393 | /// Err(error) => eprintln!("{:#?}", error),
394 | /// }
395 | /// ```
396 | pub fn prefixed<'a, C>(prefix: C) -> Prefixed<'a>
397 | where
398 | C: Into>,
399 | {
400 | Prefixed(prefix.into())
401 | }
402 |
403 | /// A type which keeps the serde field names.
404 | ///
405 | /// These types are created with the [keep_names](fn.keep_names.html) module function.
406 | pub struct KeepNames;
407 |
408 | impl KeepNames {
409 | /// Deserializes a type based on prefixed env variables
410 | pub fn from_env(&self) -> Result
411 | where
412 | T: de::DeserializeOwned,
413 | {
414 | self.from_iter(env::vars())
415 | }
416 |
417 | /// Deserializes a type based on prefixed (String, String) tuples
418 | pub fn from_iter(
419 | &self,
420 | iter: Iter,
421 | ) -> Result
422 | where
423 | T: de::DeserializeOwned,
424 | Iter: IntoIterator
- ,
425 | {
426 | let options = VarsOptions { keep_names: true };
427 | T::deserialize(Deserializer::new(iter.into_iter(), Some(options)))
428 | }
429 | }
430 |
431 | /// Produces a instance of `KeepNames` for keeping the serde field names
432 | pub fn keep_names() -> KeepNames {
433 | KeepNames {}
434 | }
435 |
436 | #[cfg(test)]
437 | mod tests {
438 | use super::*;
439 | use serde::Deserialize;
440 | use std::collections::HashMap;
441 |
442 | #[derive(Deserialize, Debug, PartialEq)]
443 | #[serde(rename_all = "lowercase")]
444 | #[derive(Default)]
445 | pub enum Size {
446 | Small,
447 | #[default]
448 | Medium,
449 | Large,
450 | }
451 |
452 | pub fn default_kaboom() -> u16 {
453 | 8080
454 | }
455 |
456 | #[derive(Deserialize, Debug, PartialEq)]
457 | pub struct CustomNewType(u32);
458 |
459 | #[derive(Deserialize, Debug, PartialEq)]
460 | pub struct Foo {
461 | bar: String,
462 | baz: bool,
463 | zoom: Option,
464 | doom: Vec,
465 | boom: Vec,
466 | #[serde(default = "default_kaboom")]
467 | kaboom: u16,
468 | #[serde(default)]
469 | debug_mode: bool,
470 | #[serde(default)]
471 | size: Size,
472 | provided: Option,
473 | newtype: CustomNewType,
474 | }
475 |
476 | #[derive(Deserialize, Debug, PartialEq)]
477 | pub struct CrazyFoo {
478 | #[serde(rename = "BaR")]
479 | bar: String,
480 | #[serde(rename = "SCREAMING_BAZ")]
481 | screaming_baz: bool,
482 | zoom: Option,
483 | }
484 |
485 | #[test]
486 | fn deserialize_from_iter() {
487 | let data = vec![
488 | (String::from("BAR"), String::from("test")),
489 | (String::from("BAZ"), String::from("true")),
490 | (String::from("DOOM"), String::from("1, 2, 3 ")),
491 | // Empty string should result in empty vector.
492 | (String::from("BOOM"), String::from("")),
493 | (String::from("SIZE"), String::from("small")),
494 | (String::from("PROVIDED"), String::from("test")),
495 | (String::from("NEWTYPE"), String::from("42")),
496 | ];
497 | match from_iter::<_, Foo>(data) {
498 | Ok(actual) => assert_eq!(
499 | actual,
500 | Foo {
501 | bar: String::from("test"),
502 | baz: true,
503 | zoom: None,
504 | doom: vec![1, 2, 3],
505 | boom: vec![],
506 | kaboom: 8080,
507 | debug_mode: false,
508 | size: Size::Small,
509 | provided: Some(String::from("test")),
510 | newtype: CustomNewType(42)
511 | }
512 | ),
513 | Err(e) => panic!("{:#?}", e),
514 | }
515 | }
516 |
517 | #[test]
518 | fn fails_with_missing_value() {
519 | let data = vec![
520 | (String::from("BAR"), String::from("test")),
521 | (String::from("BAZ"), String::from("true")),
522 | ];
523 | match from_iter::<_, Foo>(data) {
524 | Ok(_) => panic!("expected failure"),
525 | Err(e) => assert_eq!(e, Error::MissingValue("DOOM".into())),
526 | }
527 | }
528 |
529 | #[test]
530 | fn prefixed_fails_with_missing_value() {
531 | let data = vec![
532 | (String::from("PREFIX_BAR"), String::from("test")),
533 | (String::from("PREFIX_BAZ"), String::from("true")),
534 | ];
535 |
536 | match prefixed("PREFIX_").from_iter::<_, Foo>(data) {
537 | Ok(_) => panic!("expected failure"),
538 | Err(e) => assert_eq!(e, Error::MissingValue("PREFIX_DOOM".into())),
539 | }
540 | }
541 |
542 | #[test]
543 | fn fails_with_invalid_type() {
544 | let data = vec![
545 | (String::from("BAR"), String::from("test")),
546 | (String::from("BAZ"), String::from("notabool")),
547 | (String::from("DOOM"), String::from("1,2,3")),
548 | ];
549 | match from_iter::<_, Foo>(data) {
550 | Ok(_) => panic!("expected failure"),
551 | Err(e) => assert_eq!(
552 | e,
553 | Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ"))
554 | ),
555 | }
556 | }
557 |
558 | #[test]
559 | fn deserializes_from_prefixed_fieldnames() {
560 | let data = vec![
561 | (String::from("APP_BAR"), String::from("test")),
562 | (String::from("APP_BAZ"), String::from("true")),
563 | (String::from("APP_DOOM"), String::from("")),
564 | (String::from("APP_BOOM"), String::from("4,5")),
565 | (String::from("APP_SIZE"), String::from("small")),
566 | (String::from("APP_PROVIDED"), String::from("test")),
567 | (String::from("APP_NEWTYPE"), String::from("42")),
568 | ];
569 | match prefixed("APP_").from_iter::<_, Foo>(data) {
570 | Ok(actual) => assert_eq!(
571 | actual,
572 | Foo {
573 | bar: String::from("test"),
574 | baz: true,
575 | zoom: None,
576 | doom: vec![],
577 | boom: vec!["4".to_string(), "5".to_string()],
578 | kaboom: 8080,
579 | debug_mode: false,
580 | size: Size::Small,
581 | provided: Some(String::from("test")),
582 | newtype: CustomNewType(42)
583 | }
584 | ),
585 | Err(e) => panic!("{:#?}", e),
586 | }
587 | }
588 |
589 | #[test]
590 | fn prefixed_strips_prefixes() {
591 | let mut expected = HashMap::new();
592 | expected.insert("foo".to_string(), "bar".to_string());
593 | assert_eq!(
594 | prefixed("PRE_").from_iter(vec![("PRE_FOO".to_string(), "bar".to_string())]),
595 | Ok(expected)
596 | );
597 | }
598 |
599 | #[test]
600 | fn prefixed_doesnt_parse_non_prefixed() {
601 | let mut expected = HashMap::new();
602 | expected.insert("foo".to_string(), 12);
603 | assert_eq!(
604 | prefixed("PRE_").from_iter(vec![
605 | ("FOO".to_string(), "asd".to_string()),
606 | ("PRE_FOO".to_string(), "12".to_string())
607 | ]),
608 | Ok(expected)
609 | );
610 | }
611 |
612 | #[test]
613 | fn keep_names_from_iter() {
614 | let data = vec![
615 | (String::from("BaR"), String::from("test")),
616 | (String::from("SCREAMING_BAZ"), String::from("true")),
617 | (String::from("zoom"), String::from("8080")),
618 | ];
619 | match keep_names().from_iter::<_, CrazyFoo>(data) {
620 | Ok(actual) => assert_eq!(
621 | actual,
622 | CrazyFoo {
623 | bar: String::from("test"),
624 | screaming_baz: true,
625 | zoom: Some(8080),
626 | }
627 | ),
628 | Err(e) => panic!("{:#?}", e),
629 | }
630 | }
631 | }
632 |
--------------------------------------------------------------------------------