├── .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 [![Github Actions](https://github.com/softprops/envy/workflows/Main/badge.svg)](https://github.com/softprops/envy/actions) [![Coverage Status](https://coveralls.io/repos/github/softprops/envy/badge.svg?branch=master)](https://coveralls.io/github/softprops/envy?branch=master) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![crates.io](https://img.shields.io/crates/v/envy)](https://crates.io/crates/envy) [![Latest API docs](https://img.shields.io/badge/docs-latest-green.svg)](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 | --------------------------------------------------------------------------------