envy-0.4.2/.cargo_vcs_info.json0000644000000001121377445715100120660ustar { "git": { "sha1": "007a41eb27a73e2ed12e78d153027a25a61b6109" } } envy-0.4.2/.github/ISSUE_TEMPLATE/bug_report.md010064400017510000164000000010751377445713100171120ustar 00000000000000--- name: Bug report 🐛 about: Did something not work as expected? --- ## 🐛 Bug description Describe your issue in detail. #### 🤔 Expected Behavior #### 👟 Steps to reproduce #### 🌍 Your environment envy version: envy-0.4.2/.github/ISSUE_TEMPLATE/feature_request.md010064400017510000164000000005031377445713100201400ustar 00000000000000--- name: Feature request 💡 about: Suggest a new idea for envy --- ## 💡 Feature description #### 💻 Basic example envy-0.4.2/.github/PULL_REQUEST_TEMPLATE.md010064400017510000164000000013761377445713100160420ustar 00000000000000 ## What did you implement: Closes: #xxx #### How did you verify your change: #### What (if anything) would need to be called out in the CHANGELOG for the next release:envy-0.4.2/.github/workflows/main.yml010064400017510000164000000055071377445713100157450ustar 00000000000000 name: Main on: workflow_dispatch: push: paths-ignore: - '*.md' branches: - main - master tags: - '**' pull_request: paths-ignore: - '*.md' branches: - main - master env: CARGO_TERM_COLOR: always jobs: codestyle: runs-on: ubuntu-latest steps: - name: Set up Rust uses: hecrj/setup-rust-action@v1 with: components: rustfmt rust-version: nightly - uses: actions/checkout@v2 - run: cargo fmt --all -- --check lint: runs-on: ubuntu-latest steps: - name: Set up Rust uses: hecrj/setup-rust-action@v1 with: components: clippy - uses: actions/checkout@v2 - run: cargo clippy --all-targets -- -D clippy::all compile: runs-on: ubuntu-latest steps: - name: Set up Rust uses: hecrj/setup-rust-action@v1 - uses: actions/checkout@master - run: cargo check --all test: needs: [codestyle, lint, compile] strategy: matrix: rust: [stable, beta, nightly] runs-on: ubuntu-latest steps: - name: Setup Rust uses: hecrj/setup-rust-action@v1 with: rust-version: ${{ matrix.rust }} - name: Checkout uses: actions/checkout@v2 - name: Test run: cargo test - name: Coverage if: matrix.rust == 'stable' run: | # tarpaulin knows how to extract data from ci # ci services and GitHub actions is not one of them # work around that by masquerading as travis # https://github.com/xd009642/coveralls-api/blob/6da4ccd7c6eaf1df04cfd1e560362de70fa80605/src/lib.rs#L247-L262 export TRAVIS_JOB_ID=${GITHUB_SHA} export TRAVIS_PULL_REQUEST=false export TRAVIS_BRANCH=${GITHUB_REF##*/} cargo install cargo-tarpaulin cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID publish-docs: if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest needs: [test] steps: - name: Set up Rust uses: hecrj/setup-rust-action@v1 - uses: actions/checkout@v2 - name: Generate Docs shell: bash run: | cargo doc --no-deps echo "" > target/doc/index.html - name: Publish uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc publish-crate: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') needs: [test] steps: - name: Set up Rust uses: hecrj/setup-rust-action@v1 - uses: actions/checkout@v2 - name: Publish shell: bash run: cargo publish --token ${{ secrets.CRATES_TOKEN }}envy-0.4.2/.gitignore010064400017510000164000000000271377445713100126610ustar 00000000000000*.bk target Cargo.lock envy-0.4.2/CHANGELOG.md010064400017510000164000000025731377445713100125120ustar 00000000000000# 0.4.2 * Correctly deserialize empty strings into empty sequence [#51](https://github.com/softprops/envy/pull/51) # 0.4.1 * Add support for unit-variant enums as values, without using the `#[serde(field_identifier)]` attribute [#46](https://github.com/softprops/envy/pull/46) # 0.4.0 * 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) * add support for new type struct types in fields [#32](https://github.com/softprops/envy/pull/32) * fix warnings with now deprecated `trim_left_matches` [#34](https://github.com/softprops/envy/pull/34) * switch to 2018 edition rust [#37](https://github.com/softprops/envy/pull/37) # 0.3.3 * update `from_iter(..)` to accept `std::iter::IntoIterator` types This is a backwards compatible change because all Iterators have a [provided impl for IntoInterator](https://doc.rust-lang.org/src/core/iter/traits.rs.html#255-262) by default. # 0.3.2 * add new `envy::prefixed(...)` interface for prefixed env var names # 0.3.1 * fix option support # 0.3.0 * upgrade to the latest serde (1.0) # 0.2.0 * upgrade to the latest serde (0.9) # 0.1.2 * upgrade to latest serde (0.8) # 0.1.1 (2016-07-10) * allow for customization via built in serde [field annotations](https://github.com/serde-rs/serde#annotations) # 0.1.0 (2016-07-02) * initial release envy-0.4.2/CONTRIBUTING.md010064400017510000164000000053471377445713100131340ustar 00000000000000# Contributing ## Filing an Issue If you are trying to use `envy` and run into an issue- please file an issue! We'd love to get you up and running, even if the issue you have might not be directly related to the code in `envy`. This library seeks to make it easy for developers to get going, so there's a good chance we can do something to alleviate the issue by making `envy` better documented or more robust to different developer environments. When filing an issue, do your best to be as specific as possible The faster was can reproduce your issue, the faster we can fix it for you! ## Submitting a PR If you are considering filing a pull request, make sure that there's an issue filed for the work you'd like to do. There might be some discussion required! Filing an issue first will help ensure that the work you put into your pull request will get merged :) Before you submit your pull request, check that you have completed all of the steps mentioned in the pull request template. Link the issue that your pull request is responding to, and format your code using [rustfmt][rustfmt]. ### Configuring rustfmt Before submitting code in a PR, make sure that you have formatted the codebase using [rustfmt][rustfmt]. `rustfmt` is a tool for formatting Rust code, which helps keep style consistent across the project. If you have not used `rustfmt` before, it is not too difficult. If you have not already configured `rustfmt` for the nightly toolchain, it can be done using the following steps: **1. Use Nightly Toolchain** Install the nightly toolchain. This will only be necessary as long as rustfmt produces different results on stable and nightly. ```sh $ rustup toolchain install nightly ``` **2. Add the rustfmt component** Install the most recent version of `rustfmt` using this command: ```sh $ rustup component add rustfmt-preview --toolchain nightly ``` **3. Running rustfmt** To run `rustfmt`, use this command: ```sh cargo +nightly fmt ``` [rustfmt]: https://github.com/rust-lang-nursery/rustfmt ### IDE Configuration files Machine specific configuration files may be generaged 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 commited to the project and causing issues for other contributors. 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. For help setting up a global .gitignore check out this [GitHub article]! [GitHub article]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore ## Conduct This project follows the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html)envy-0.4.2/Cargo.lock0000644000000030051377445715100100450ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "envy" version = "0.4.2" dependencies = [ "serde", ] [[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] [[package]] name = "quote" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2", ] [[package]] name = "serde" version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4211ce9909eb971f111059df92c45640aad50a619cf55cd76476be803c4c68e6" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" envy-0.4.2/Cargo.toml0000644000000021441377445715100100730ustar # 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 believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "envy" version = "0.4.2" authors = ["softprops "] description = "deserialize env vars into typesafe structs" homepage = "https://github.com/softprops/envy" documentation = "https://softprops.github.io/envy" readme = "README.md" keywords = ["serde", "env"] categories = ["config"] license = "MIT" repository = "https://github.com/softprops/envy" [dependencies.serde] version = "1.0" [dev-dependencies.serde] version = "1.0" features = ["derive"] [badges.coveralls] repository = "softprops/envy" [badges.travis-ci] repository = "softprops/envy" envy-0.4.2/Cargo.toml.orig010064400017510000164000000011361377445713100135620ustar 00000000000000[package] name = "envy" version = "0.4.2" authors = ["softprops "] description = "deserialize env vars into typesafe structs" documentation = "https://softprops.github.io/envy" homepage = "https://github.com/softprops/envy" repository = "https://github.com/softprops/envy" keywords = ["serde", "env"] license = "MIT" readme = "README.md" edition = "2018" categories = [ "config" ] [badges] coveralls = { repository = "softprops/envy" } travis-ci = { repository = "softprops/envy" } [dependencies] serde = "1.0" [dev-dependencies] serde = { version = "1.0", features = ["derive"] }envy-0.4.2/LICENSE010064400017510000164000000020451377445713100117000ustar 00000000000000Copyright (c) 2016-2019 Doug Tangren 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. envy-0.4.2/Makefile010064400017510000164000000001731377445713100123330ustar 00000000000000 test-coverage: @docker run -it --rm \ --security-opt seccomp=unconfined \ -v "$(PWD):/volume" \ xd009642/tarpaulin envy-0.4.2/README.md010064400017510000164000000067261377445713100121640ustar 00000000000000# 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](http://meritbadge.herokuapp.com/envy)](https://crates.io/crates/envy) [![Latest API docs](https://img.shields.io/badge/docs-latest-green.svg)](https://softprops.github.io/envy) > deserialize environment variables into typesafe structs ## 📦 install Add the following to your `Cargo.toml` file. ```toml [dependencies] envy = "0.4" ``` ## 🤸 usage A typical envy usage looks like the following. Assuming your rust program looks something like this... > 💡 These examples use Serde's [derive feature](https://serde.rs/derive.html) ```rust use serde::Deserialize; #[derive(Deserialize, Debug)] struct Config { foo: u16, bar: bool, baz: String, boom: Option } fn main() { match envy::from_env::() { Ok(config) => println!("{:#?}", config), Err(error) => panic!("{:#?}", error) } } ``` ... export some environment variables ```bash $ FOO=8080 BAR=true BAZ=hello yourapp ``` You should be able to access a completely typesafe config struct deserialized from env vars. 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`. Structs with `Option` type fields will successfully be deserialized when their associated env var is absent. Envy also supports deserializing `Vecs` from comma separated env var values. Because envy is built on top of serde, you can use all of serde's [attributes](https://serde.rs/attributes.html) to your advantage. For instance let's say your app requires a field but would like a sensible default when one is not provided. ```rust /// provides default value for zoom if ZOOM env var is not set fn default_zoom() -> { 32 } #[derive(Deserialize, Debug)] struct Config { foo: u16, bar: bool, baz: String, boom: Option, #[serde(default="default_zoom")] zoom: u16 } ``` The following will yield an application configured with a zoom of 32 ```bash $ FOO=8080 BAR=true BAZ=hello yourapp ``` The following will yield an application configured with a zoom of 10 ```bash $ FOO=8080 BAR=true BAZ=hello ZOOM=10 yourapp ``` The common pattern for prefixing env var names for a specific app is supported using the `envy::prefixed(prefix)` interface. Asumming your env vars are prefixed with `APP_` the above example may instead look like ```rust use serde::Deserialize; #[derive(Deserialize, Debug)] struct Config { foo: u16, bar: bool, baz: String, boom: Option } fn main() { match envy::prefixed("APP_").from_env::() { Ok(config) => println!("{:#?}", config), Err(error) => panic!("{:#?}", error) } } ``` the expectation would then be to export the same environment variables prefixed with `APP_` ```bash $ APP_FOO=8080 APP_BAR=true APP_BAZ=hello yourapp ``` > 👭 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. Doug Tangren (softprops) 2016-2019 envy-0.4.2/examples/optional.rs010064400017510000164000000004471377445713100147100ustar 00000000000000use serde::Deserialize; #[derive(Deserialize)] struct Config { size: Option, } fn main() { match envy::from_env::() { Ok(config) => println!("provided config.size {:?}", config.size), Err(err) => println!("error parsing config from env: {}", err), } } envy-0.4.2/examples/prefixed.rs010064400017510000164000000004701377445713100146650ustar 00000000000000use serde::Deserialize; #[derive(Deserialize)] struct Config { bar: Option, } fn main() { match envy::prefixed("FOO_").from_env::() { Ok(config) => println!("provided config.bar {:?}", config.bar), Err(err) => println!("error parsing config from env: {}", err), } } envy-0.4.2/rustfmt.toml010064400017510000164000000006711377445713100132770ustar 00000000000000# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_args_layout fn_args_layout = "Vertical" # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports merge_imports = true # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#format_code_in_doc_comments format_code_in_doc_comments = true # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#edition edition = "2018"envy-0.4.2/src/error.rs010064400017510000164000000025301377445713100131600ustar 00000000000000//! Error types use serde::de::Error as SerdeError; use std::{error::Error as StdError, fmt}; /// Types of errors that may result from failed attempts /// to deserialize a type from env vars #[derive(Debug, Clone, PartialEq)] pub enum Error { MissingValue(&'static str), Custom(String), } impl StdError for Error {} impl fmt::Display for Error { fn fmt( &self, fmt: &mut fmt::Formatter, ) -> fmt::Result { match *self { Error::MissingValue(field) => write!(fmt, "missing value for field {}", field), Error::Custom(ref msg) => write!(fmt, "{}", msg), } } } impl SerdeError for Error { fn custom(msg: T) -> Self { Error::Custom(format!("{}", msg)) } fn missing_field(field: &'static str) -> Error { Error::MissingValue(field) } } #[cfg(test)] mod tests { use super::*; fn impl_std_error(_: E) {} #[test] fn error_impl_std_error() { impl_std_error(Error::MissingValue("foo_bar")); impl_std_error(Error::Custom("whoops".into())) } #[test] fn error_display() { assert_eq!( format!("{}", Error::MissingValue("foo_bar")), "missing value for field foo_bar" ); assert_eq!(format!("{}", Error::Custom("whoops".into())), "whoops") } } envy-0.4.2/src/lib.rs010064400017510000164000000336161377445713100126060ustar 00000000000000//! Envy is a library for deserializing environment variables into typesafe structs //! //! # Examples //! //! A typical usecase for envy is deserializing configuration store in an process' environment into a struct //! whose fields map to the names of env vars. //! //! Serde makes it easy to provide a deserializable struct with its [deriveable Deserialize](https://serde.rs/derive.html) //! procedural macro. //! //! Simply ask for an instance of that struct from envy's `from_env` function. //! //! ```no_run //! use serde::Deserialize; //! //! #[derive(Deserialize, Debug)] //! struct Config { //! foo: u16, //! bar: bool, //! baz: String, //! boom: Option, //! } //! //! match envy::from_env::() { //! Ok(config) => println!("{:#?}", config), //! Err(error) => eprintln!("{:#?}", error), //! } //! ``` //! //! Special treatment is given to collections. For config fields that store a `Vec` of values, //! use an env var that uses a comma separated value. //! //! All serde modifiers should work as is. //! //! Enums with unit variants can be used as values: //! //! ```no_run //! # use serde::Deserialize; //! //! #[derive(Deserialize, Debug, PartialEq)] //! #[serde(rename_all = "lowercase")] //! pub enum Size { //! Small, //! Medium, //! Large, //! } //! //! #[derive(Deserialize, Debug)] //! struct Config { //! size: Size, //! } //! //! // set env var for size as `SIZE=medium` //! match envy::from_env::() { //! Ok(config) => println!("{:#?}", config), //! Err(error) => eprintln!("{:#?}", error), //! } //! ``` use serde::de::{ self, value::{MapDeserializer, SeqDeserializer}, IntoDeserializer, }; use std::{ borrow::Cow, env, iter::{empty, IntoIterator}, }; // Ours mod error; pub use crate::error::Error; /// A type result type specific to `envy::Errors` pub type Result = std::result::Result; struct Vars(Iter) where Iter: IntoIterator; struct Val(String, String); impl<'de> IntoDeserializer<'de, Error> for Val { type Deserializer = Self; fn into_deserializer(self) -> Self::Deserializer { self } } struct VarName(String); impl<'de> IntoDeserializer<'de, Error> for VarName { type Deserializer = Self; fn into_deserializer(self) -> Self::Deserializer { self } } impl> Iterator for Vars { type Item = (VarName, Val); fn next(&mut self) -> Option { self.0 .next() .map(|(k, v)| (VarName(k.to_lowercase()), Val(k, v))) } } macro_rules! forward_parsed_values { ($($ty:ident => $method:ident,)*) => { $( fn $method(self, visitor: V) -> Result where V: de::Visitor<'de> { match self.1.parse::<$ty>() { Ok(val) => val.into_deserializer().$method(visitor), Err(e) => Err(de::Error::custom(format_args!("{} while parsing value '{}' provided by {}", e, self.1, self.0))) } } )* } } impl<'de> de::Deserializer<'de> for Val { type Error = Error; fn deserialize_any( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.1.into_deserializer().deserialize_any(visitor) } fn deserialize_seq( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { // std::str::split doesn't work as expected for our use case: when we // get an empty string we want to produce an empty Vec, but split would // still yield an iterator with an empty string in it. So we need to // special case empty strings. if self.1.is_empty() { SeqDeserializer::new(empty::()).deserialize_seq(visitor) } else { let values = self.1.split(',').map(|v| Val(self.0.clone(), v.to_owned())); SeqDeserializer::new(values).deserialize_seq(visitor) } } fn deserialize_option( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_some(self) } forward_parsed_values! { bool => deserialize_bool, u8 => deserialize_u8, u16 => deserialize_u16, u32 => deserialize_u32, u64 => deserialize_u64, i8 => deserialize_i8, i16 => deserialize_i16, i32 => deserialize_i32, i64 => deserialize_i64, f32 => deserialize_f32, f64 => deserialize_f64, } #[inline] fn deserialize_newtype_struct( self, _: &'static str, visitor: V, ) -> Result where V: serde::de::Visitor<'de>, { visitor.visit_newtype_struct(self) } fn deserialize_enum( self, _name: &'static str, _variants: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_enum(self.1.into_deserializer()) } serde::forward_to_deserialize_any! { char str string unit bytes byte_buf map unit_struct tuple_struct identifier tuple ignored_any struct } } impl<'de> de::Deserializer<'de> for VarName { type Error = Error; fn deserialize_any( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.0.into_deserializer().deserialize_any(visitor) } #[inline] fn deserialize_newtype_struct( self, _: &'static str, visitor: V, ) -> Result where V: serde::de::Visitor<'de>, { visitor.visit_newtype_struct(self) } serde::forward_to_deserialize_any! { char str string unit seq option bytes byte_buf map unit_struct tuple_struct identifier tuple ignored_any enum struct bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 } } /// A deserializer for env vars struct Deserializer<'de, Iter: Iterator> { inner: MapDeserializer<'de, Vars, Error>, } impl<'de, Iter: Iterator> Deserializer<'de, Iter> { fn new(vars: Iter) -> Self { Deserializer { inner: MapDeserializer::new(Vars(vars)), } } } impl<'de, Iter: Iterator> de::Deserializer<'de> for Deserializer<'de, Iter> { type Error = Error; fn deserialize_any( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_map(visitor) } fn deserialize_map( self, visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_map(self.inner) } serde::forward_to_deserialize_any! { bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq bytes byte_buf unit_struct tuple_struct identifier tuple ignored_any option newtype_struct enum struct } } /// Deserializes a type based on information stored in env variables pub fn from_env() -> Result where T: de::DeserializeOwned, { from_iter(env::vars()) } /// Deserializes a type based on an iterable of `(String, String)` /// representing keys and values pub fn from_iter(iter: Iter) -> Result where T: de::DeserializeOwned, Iter: IntoIterator, { T::deserialize(Deserializer::new(iter.into_iter())) } /// A type which filters env vars with a prefix for use as serde field inputs /// /// These types are created with with the [prefixed](fn.prefixed.html) module function pub struct Prefixed<'a>(Cow<'a, str>); impl<'a> Prefixed<'a> { /// Deserializes a type based on prefixed env variables pub fn from_env(&self) -> Result where T: de::DeserializeOwned, { self.from_iter(env::vars()) } /// Deserializes a type based on prefixed (String, String) tuples pub fn from_iter( &self, iter: Iter, ) -> Result where T: de::DeserializeOwned, Iter: IntoIterator, { crate::from_iter(iter.into_iter().filter_map(|(k, v)| { if k.starts_with(self.0.as_ref()) { Some((k.trim_start_matches(self.0.as_ref()).to_owned(), v)) } else { None } })) } } /// Produces a instance of `Prefixed` for prefixing env variable names /// /// # Example /// /// ```no_run /// use serde::Deserialize; /// /// #[derive(Deserialize, Debug)] /// struct Config { /// foo: u16, /// bar: bool, /// baz: String, /// boom: Option, /// } /// /// // all env variables will be expected to be prefixed with APP_ /// // i.e. APP_FOO, APP_BAR, ect /// match envy::prefixed("APP_").from_env::() { /// Ok(config) => println!("{:#?}", config), /// Err(error) => eprintln!("{:#?}", error), /// } /// ``` pub fn prefixed<'a, C>(prefix: C) -> Prefixed<'a> where C: Into>, { Prefixed(prefix.into()) } #[cfg(test)] mod tests { use super::*; use serde::Deserialize; use std::collections::HashMap; #[derive(Deserialize, Debug, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Size { Small, Medium, Large, } impl Default for Size { fn default() -> Size { Size::Medium } } pub fn default_kaboom() -> u16 { 8080 } #[derive(Deserialize, Debug, PartialEq)] pub struct CustomNewType(u32); #[derive(Deserialize, Debug, PartialEq)] pub struct Foo { bar: String, baz: bool, zoom: Option, doom: Vec, boom: Vec, #[serde(default = "default_kaboom")] kaboom: u16, #[serde(default)] debug_mode: bool, #[serde(default)] size: Size, provided: Option, newtype: CustomNewType, } #[test] fn deserialize_from_iter() { let data = vec![ (String::from("BAR"), String::from("test")), (String::from("BAZ"), String::from("true")), (String::from("DOOM"), String::from("1,2,3")), // Empty string should result in empty vector. (String::from("BOOM"), String::from("")), (String::from("SIZE"), String::from("small")), (String::from("PROVIDED"), String::from("test")), (String::from("NEWTYPE"), String::from("42")), ]; match from_iter::<_, Foo>(data) { Ok(actual) => assert_eq!( actual, Foo { bar: String::from("test"), baz: true, zoom: None, doom: vec![1, 2, 3], boom: vec![], kaboom: 8080, debug_mode: false, size: Size::Small, provided: Some(String::from("test")), newtype: CustomNewType(42) } ), Err(e) => panic!("{:#?}", e), } } #[test] fn fails_with_missing_value() { let data = vec![ (String::from("BAR"), String::from("test")), (String::from("BAZ"), String::from("true")), ]; match from_iter::<_, Foo>(data) { Ok(_) => panic!("expected failure"), Err(e) => assert_eq!(e, Error::MissingValue("doom")), } } #[test] fn fails_with_invalid_type() { let data = vec![ (String::from("BAR"), String::from("test")), (String::from("BAZ"), String::from("notabool")), (String::from("DOOM"), String::from("1,2,3")), ]; match from_iter::<_, Foo>(data) { Ok(_) => panic!("expected failure"), Err(e) => assert_eq!( e, Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ")) ), } } #[test] fn deserializes_from_prefixed_fieldnames() { let data = vec![ (String::from("APP_BAR"), String::from("test")), (String::from("APP_BAZ"), String::from("true")), (String::from("APP_DOOM"), String::from("")), (String::from("APP_BOOM"), String::from("4,5")), (String::from("APP_SIZE"), String::from("small")), (String::from("APP_PROVIDED"), String::from("test")), (String::from("APP_NEWTYPE"), String::from("42")), ]; match prefixed("APP_").from_iter::<_, Foo>(data) { Ok(actual) => assert_eq!( actual, Foo { bar: String::from("test"), baz: true, zoom: None, doom: vec![], boom: vec!["4".to_string(), "5".to_string()], kaboom: 8080, debug_mode: false, size: Size::Small, provided: Some(String::from("test")), newtype: CustomNewType(42) } ), Err(e) => panic!("{:#?}", e), } } #[test] fn prefixed_strips_prefixes() { let mut expected = HashMap::new(); expected.insert("foo".to_string(), "bar".to_string()); assert_eq!( prefixed("PRE_").from_iter(vec![("PRE_FOO".to_string(), "bar".to_string())]), Ok(expected) ); } #[test] fn prefixed_doesnt_parse_non_prefixed() { let mut expected = HashMap::new(); expected.insert("foo".to_string(), 12); assert_eq!( prefixed("PRE_").from_iter(vec![ ("FOO".to_string(), "asd".to_string()), ("PRE_FOO".to_string(), "12".to_string()) ]), Ok(expected) ); } }