diff --git a/micronfig/src/cache.rs b/micronfig/src/cache.rs index b308746..83ee089 100644 --- a/micronfig/src/cache.rs +++ b/micronfig/src/cache.rs @@ -1,14 +1,24 @@ -//! Definition of [`Cache`]. +//! **Private**; definition of [`Cache`]. use std::ffi::OsStr; use std::fmt::Debug; /// Cache initialized only once per config block and used to quickly retrieve configuration values. +/// +/// Every `env*` feature has its own field here, which may or may not be used. #[derive(Clone, Default, Debug)] pub struct Cache { + /// Unused. + #[cfg(feature = "envvars")] + pub envvars: (), + + /// Unused. + #[cfg(feature = "envfiles")] + pub envfiles: (), + /// `.env` file cache, in order of access priority. /// - /// More can be added with [`Cache::register_dotenv`]. + /// More can be added with [`Cache::envdot_register`]. #[cfg(feature = "envdot")] pub envdot: Vec } @@ -19,14 +29,20 @@ impl Cache { let mut this = Self::default(); if cfg!(feature = "envdot") { - this.register_dotenv("./.env.local"); - this.register_dotenv("./.env"); + this.envdot_register("./.env.local"); + this.envdot_register("./.env"); } this } /// Get a value from the cache. + /// + /// The following sources, if the respective feature is enabled, are checked in the following order: + /// 1. `envfiles` + /// 2. `envvars` + /// 3. `envdot` + /// pub fn get(&self, key: &OsStr) -> Option { let mut value = None; @@ -53,7 +69,7 @@ impl Cache { /// Register a new `.env` file in the cache, if it exists. #[cfg(feature = "envdot")] - pub fn register_dotenv(&mut self, path: Path) + pub fn envdot_register(&mut self, path: Path) where Path: AsRef + Debug { let dotenv = crate::envdot::parse_dotenv(path); @@ -62,3 +78,112 @@ impl Cache { } } } + +//noinspection DotEnvSpaceAroundSeparatorInspection +#[cfg(test)] +mod tests { + use crate::testing::tempfile_fixture; + use super::*; + + #[cfg(feature = "envdot")] + #[test] + fn envdot_register() { + let file = tempfile_fixture( + // language=dotenv + r#" + GARAS=garas + export AUTO= auto + BUS = bus + "# + ); + + let mut cache = Cache::default(); + cache.envdot_register(file.as_os_str()); + + assert_eq!(cache.envdot.len(), 1); + } + + #[cfg(feature = "envvars")] + #[test] + fn get_envvars() { + std::env::set_var("GARAS", "garas"); + std::env::remove_var("GARAS_FILE"); + + let cache = Cache::default(); + assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string())); + } + + #[cfg(feature = "envfiles")] + #[test] + fn get_envfiles() { + let file = tempfile_fixture("garas"); + std::env::remove_var("GARAS"); + std::env::set_var("GARAS_FILE", file.as_os_str()); + + let cache = Cache::default(); + assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string())); + } + + #[cfg(feature = "envdot")] + #[test] + fn get_envdot() { + std::env::remove_var("GARAS"); + std::env::remove_var("GARAS_FILE"); + let file = tempfile_fixture( + // language=dotenv + r#"GARAS=garas"# + ); + + let mut cache = Cache::default(); + cache.envdot_register(file.as_os_str()); + assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string())); + } + + #[test] + fn priority() { + let mut cache = Cache::default(); + + let envfiles_file = tempfile_fixture("envfiles"); + + let envdot_file = tempfile_fixture( + // language=dotenv + r#" + export ENVFILES=envdot + export ENVVARS=envdot + export ENVDOT=envdot + "# + ); + + if cfg!(feature = "envfiles") { + std::env::set_var("ENVFILES_FILE", envfiles_file.as_os_str()); + std::env::remove_var("ENVVARS_FILE"); + std::env::remove_var("ENVDOT_FILE"); + std::env::remove_var("NONE_FILE"); + } + + if cfg!(feature = "envvars") { + std::env::set_var("ENVFILES", "envvars"); + std::env::set_var("ENVVARS", "envvars"); + std::env::remove_var("ENVDOT"); + std::env::remove_var("NONE"); + } + + if cfg!(feature = "envdot") { + cache.envdot_register(envdot_file.as_os_str()); + } + + if cfg!(feature = "envfiles") { + assert_eq!(cache.get("ENVFILES".as_ref()), Some("envfiles".to_string())); + } + + if cfg!(feature = "envvars") { + assert_eq!(cache.get("ENVVARS".as_ref()), Some("envvars".to_string())); + } + + if cfg!(feature = "envdot") { + assert_eq!(cache.get("ENVDOT".as_ref()), Some("envdot".to_string())); + } + + assert_eq!(cache.get("NONE".as_ref()), None); + } +} diff --git a/micronfig/src/envdot.rs b/micronfig/src/envdot.rs index 2f9c8ed..211ec97 100644 --- a/micronfig/src/envdot.rs +++ b/micronfig/src/envdot.rs @@ -1,4 +1,4 @@ -//! Utilities for fetching configuration values defined in specific `.env` files. +//! **Private**; utilities for fetching configuration values defined in specific `.env` files. use std::collections::HashMap; use std::ffi::{OsStr, OsString}; @@ -25,14 +25,14 @@ pub fn parse_dotenv

(value: P) -> Option let mut keys: HashMap = HashMap::new(); - let re = Regex::new(r#"^(?:export\s+)?([^=]+)\s*=\s*(.+)$"#) + let re = Regex::new(r#"^\s*(?:export\s)?\s*([^=]+?)\s*=\s*(.+)\s*$"#) .expect("Regex to be valid"); - let _ = contents.split("\n") + contents.split("\n") .filter_map(|line| re.captures(line)) .map(|capture| { - let key = &capture[0]; - let value = &capture[1]; + let key = &capture[1]; + let value = &capture[2]; if value.starts_with('\'') && value.ends_with('\'') { ( @@ -63,7 +63,9 @@ pub fn parse_dotenv

(value: P) -> Option ) } }) - .map(|(key, value)| keys.insert(key, value)); + .for_each(|(key, value)| { + keys.insert(key, value); + }); Some(keys) } @@ -73,3 +75,94 @@ pub fn get(dotenv: &DotEnv, key: &OsStr) -> Option { dotenv.get(key).map(|v| v.to_owned()) } + +//noinspection DotEnvSpaceAroundSeparatorInspection +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::tempfile_fixture; + + #[test] + fn dotenv_simple() { + let file = tempfile_fixture( + // language=dotenv + r#" + GARAS=garas + AUTO= auto + BUS = bus + "# + ); + + let parsed = parse_dotenv(file); + + let mut compared: HashMap = HashMap::new(); + compared.insert("GARAS".into(), "garas".into()); + compared.insert("AUTO".into(), "auto".into()); + compared.insert("BUS".into(), "bus".into()); + + assert_eq!(parsed, Some(compared)); + } + + #[test] + fn dotenv_apos() { + let file = tempfile_fixture( + // language=dotenv + r#" + GARAS='garas' + AUTO= 'auto' + BUS = 'bus' + "# + ); + + let parsed = parse_dotenv(file); + + let mut compared: HashMap = HashMap::new(); + compared.insert("GARAS".into(), "garas".into()); + compared.insert("AUTO".into(), "auto".into()); + compared.insert("BUS".into(), "bus".into()); + + assert_eq!(parsed, Some(compared)); + } + + #[test] + fn dotenv_quote() { + let file = tempfile_fixture( + // language=dotenv + r#" + GARAS="garas" + AUTO= "auto" + BUS = "bus" + "# + ); + + let parsed = parse_dotenv(file); + + let mut compared: HashMap = HashMap::new(); + compared.insert("GARAS".into(), "garas".into()); + compared.insert("AUTO".into(), "auto".into()); + compared.insert("BUS".into(), "bus".into()); + + assert_eq!(parsed, Some(compared)); + } + + #[test] + fn dotenv_export() { + let file = tempfile_fixture( + // language=dotenv + r#" + export GARAS=garas + export AUTO= auto + export BUS = bus + "# + ); + + let parsed = parse_dotenv(file); + + let mut compared: HashMap = HashMap::new(); + compared.insert("GARAS".into(), "garas".into()); + compared.insert("AUTO".into(), "auto".into()); + compared.insert("BUS".into(), "bus".into()); + + assert_eq!(parsed, Some(compared)); + } +} \ No newline at end of file diff --git a/micronfig/src/envfiles.rs b/micronfig/src/envfiles.rs index 1d3135f..f2d41cc 100644 --- a/micronfig/src/envfiles.rs +++ b/micronfig/src/envfiles.rs @@ -1,4 +1,4 @@ -//! Contents of files at paths defined by environment variables. +//! **Private**; utilities for fetching configuration values from contents of files at paths defined by environment variables. use std::ffi::OsStr; use std::io::Read; @@ -22,7 +22,6 @@ pub fn get(key: &OsStr) -> Option { Some(data) } - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/micronfig/src/envvars.rs b/micronfig/src/envvars.rs index 5a9a8d0..fd576f2 100644 --- a/micronfig/src/envvars.rs +++ b/micronfig/src/envvars.rs @@ -1,4 +1,4 @@ -//! Environment variables. +//! **Private**; utilities for fetching configuration values from environment variables. use std::ffi::OsStr; @@ -7,7 +7,6 @@ pub fn get(key: &OsStr) -> Option { std::env::var(key).ok() } - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/micronfig/src/lib.rs b/micronfig/src/lib.rs index 17d1981..2660f9a 100644 --- a/micronfig/src/lib.rs +++ b/micronfig/src/lib.rs @@ -1,12 +1,190 @@ +//! Crate for macro-based configuration management. +//! +//! ## Description +//! +//! This crate and it's sister [`micronfig_macros`] combine to provide the [`config`] macro, which allows the developer to define all configuration variables required by an application in a single place and have them expanded to static references of the desired types. +//! +//! ``` +//! micronfig::config! { +//! DATABASE_URI, +//! APPLICATION_NAME: String, +//! MAX_CONCURRENT_USERS: String > u64, +//! SHOWN_ALERT?, +//! } +//! ``` +//! +//! ## Examples +//! +//! ### Strings configuration +//! +//! To define configuration variables returning a string, create a [`config`] block and list their names separated by commas `,`: +//! +//! ``` +//! micronfig::config! { +//! VARIABLE_A, +//! VARIABLE_B, +//! VARIABLE_C, +//! PATH, +//! } +//! ``` +//! +//! To access them, call their name as if it was a function: +//! +//! ``` +//! # micronfig::config! { +//! # VARIABLE_A, +//! # VARIABLE_B, +//! # VARIABLE_C, +//! # PATH, +//! # } +//! # +//! # std::env::set_var("VARIABLE_A", "a"); +//! # std::env::set_var("VARIABLE_B", "b"); +//! # std::env::set_var("VARIABLE_C", "c"); +//! # std::env::set_var("PATH", "/bin"); +//! # +//! // These will all return `&'static str` values. +//! println!("{}", VARIABLE_A()); +//! println!("{}", VARIABLE_B()); +//! println!("{}", VARIABLE_C()); +//! println!("{}", PATH()); +//! ``` +//! +//! > ***Note*** +//! > +//! > Both the [`config`] block and variables defined in it are lazily initialized on first call. +//! > +//! > The first time one of these functions is called, configuration files will be parsed, and the first time each is called, its value is retrieved and stored. +//! +//! ### Required and optional variables +//! +//! By default, configuration variables are all required, causing a [panic] if their value is missing the first time their function is called. +//! +//! Configuration variables can be marked as [Option]al by suffixing a question mark `?` to their name, making them return a `&'static` [`Option`] instead: +//! +//! ``` +//! micronfig::config! { +//! VARIABLE_REQUIRED, +//! VARIABLE_OPTIONAL?, +//! } +//! ``` +//! +//! ### Conversions +//! +//! All variables are read from their source as strings; therefore, the following explicit syntax for defining them is supported: +//! +//! ``` +//! micronfig::config! { +//! VARIABLE_A: String, +//! VARIABLE_B: String, +//! VARIABLE_C: String, +//! } +//! ``` +//! +//! Strings are not the best option for most situations, so the crate makes use of some traits to allow their conversion to different types: +//! +//! | Trait | Symbol | Notes | +//! |---|---|---| +//! | [`From`] | `->` | | +//! | [`TryFrom`] | `=>` | Will panic if the conversion fails. | +//! | [`std::str::FromStr`] | `>` | Will panic if the parsing fails. | +//! +//! The syntax for conversion is as follows: +//! +//! ``` +//! micronfig::config! { +//! // use FromStr to parse the String as an isize +//! REQUIRED_SIGNED: String > isize, +//! // use FromStr to parse the String as a SocketAddr +//! REQUIRED_SOCKETADDR: String > std::net::SocketAddr, +//! // use From to convert the String to... another String +//! REQUIRED_STRING: String -> String, +//! // use TryFrom to convert the String to another String +//! // (there aren't many types in std to make valid examples from!) +//! REQUIRED_TRYSTRING: String => String, +//! // the conversion will not be performed for missing optional variables +//! OPTIONAL_UNSIGNED?: String > usize, +//! } +//! ``` +//! +//! > ***Warning*** +//! > +//! > Types should always be fully qualified, or the macro won't work properly! +//! +//! Custom types can be used as well: +//! +//! ``` +//! struct Duplicator { +//! copy_a: String, +//! copy_b: String, +//! } +//! +//! impl From for Duplicator { +//! fn from(value: String) -> Self { +//! Self { +//! copy_a: value.clone(), +//! copy_b: value +//! } +//! } +//! } +//! +//! micronfig::config! { +//! MY_CUSTOM_TYPE: String -> crate::Duplicator, +//! } +//! +//! # fn main() {} +//! ``` +//! +//! Conversions can be chained too: +//! +//! ``` +//! struct ChatId(u64); +//! +//! impl From for ChatId { +//! fn from(value: u64) -> Self { +//! Self(value) +//! } +//! } +//! +//! micronfig::config! { +//! // First parse the string as an u64 with FromStr, then convert it to a ChatId with From. +//! RESPOND_TO_MESSAGES_IN: String > u64 -> crate::ChatId, +//! } +//! +//! # fn main() {} +//! ``` +//! +//! ## Crate features +//! +//! ### Value sources +//! +//! The crate supports retrieving values from various different sources depending on the needs of the application. +//! +//! The sources can be toggled on and off via crate features, and are listed in the following table in order of retrieval priority, where the topmost one is the first source that is checked, and the following ones are checked only if no value is detected in the ones above. +//! +//! | Feature | Description | Use case | +//! |---|---|---| +//! | `envfiles` | Contents of the file at the path indicated by the `{NAME}_FILE` environment variable. | Docker [configs](https://docs.docker.com/engine/swarm/configs/) and [secrets](https://docs.docker.com/engine/swarm/secrets/). | +//! | `envvars` | The `{NAME}` environment variable. | Most command-line applications. | +//! | `envdot` | The `.env` and `.env.local` files in the current working directory. | Application development. | +//! +//! By default, all of them are enabled. +//! + +/// The macro described [at the crate's root](micronfig). +pub use micronfig_macros::config; + pub mod cache; #[cfg(feature = "envvars")] pub mod envvars; + #[cfg(feature = "envfiles")] pub mod envfiles; + #[cfg(feature = "envdot")] pub mod envdot; -#[cfg(test)] -mod testing; -pub use micronfig_macros::config; +#[cfg(test)] +pub mod testing; + diff --git a/micronfig/src/testing.rs b/micronfig/src/testing.rs index 58e40f4..88e005b 100644 --- a/micronfig/src/testing.rs +++ b/micronfig/src/testing.rs @@ -1,3 +1,5 @@ +/// **Private**; utilities for testing. + pub fn tempfile_fixture(content: &str) -> tempfile::TempPath { use std::io::Write;