From ef0094cd2c4247304ec261b16b85138e10df3dfe Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 30 Dec 2023 03:31:15 +0100 Subject: [PATCH] Complete the new crate, but... --- Cargo.toml | 16 ++--- src/cache.rs | 22 +++++++ src/lib.rs | 143 ++++++++++++++++++++++++++++++++++++++++ src/sources/envdot.rs | 50 ++++++++++++++ src/sources/envfiles.rs | 21 ++++++ src/sources/envvars.rs | 8 +++ src/sources/mod.rs | 8 +++ 7 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 src/cache.rs create mode 100644 src/lib.rs create mode 100644 src/sources/envdot.rs create mode 100644 src/sources/envfiles.rs create mode 100644 src/sources/envvars.rs create mode 100644 src/sources/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 5a15099..8757bd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,15 +17,13 @@ rustdoc-args = ["--document-private-items", "--cfg", "docsrs"] [features] -default = ["single_envvars", "single_envfiles", "multi", "handle", "macros"] -single_envvars = [] -single_envfiles = [] -multi = ["single_envvars", "single_envfiles"] -handle = ["multi"] -macros = ["lazy_static", "handle"] +default = ["envvars", "envfiles", "envdot"] +envvars = [] +envfiles = [] +envdot = ["regex"] testing = ["tempfile"] - [dependencies] -lazy_static = { version = "1.4.0", optional = true } -tempfile = { version = "3.5.0", optional = true } +tempfile = { version = "3.9.0", optional = true } +regex = { version = "1.10.2", optional = true } + diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..7e9a127 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,22 @@ +use std::fmt::Debug; +use std::path::Path; + + +#[derive(Clone, Default, Debug)] +pub struct MicronfigCache { + #[cfg(feature = "envdot")] + pub dotenvs: Vec +} + +impl MicronfigCache { + #[cfg(feature = "envdot")] + pub fn add_envdot

(&mut self, path: P) + where P: AsRef + Debug + { + self.dotenvs.push( + crate::sources::envdot::DotEnv::from( + path + ) + ); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..231658e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,143 @@ +use std::fmt::Debug; + +pub mod sources; +pub mod cache; + +#[macro_export] +macro_rules! __micronfig_last { + [ $head:ty, $( $tail:ty, )+ ] => { + $crate::__micronfig_last![ $( $tail, )+ ] + }; + [ $head:ty, ] => { + $head + } +} + +/// # Examples +/// +/// ## Get a string directly +/// +/// ``` +/// micronfig::config! { +/// MY_STRING_A: String, +/// MY_STRING_B: String +/// } +/// ``` +/// +/// ## Parse envvar as a number +/// +/// ``` +/// micronfig::config! { +/// MY_UNSIGNED_NUMBER: u32, +/// MY_SIGNED_NUMBER: i32 +/// } +/// ``` +/// +/// ## Parse string as an IpAddr +/// +/// ``` +/// use std::net::IpAddr; +/// +/// micronfig::config! { +/// MY_IP_ADDR: IpAddr +/// } +/// ``` +/// +/// ## Parse string as a number, then convert it into an Ipv4Addr +/// +/// ``` +/// use std::net::Ipv4Addr; +/// +/// micronfig::config! { +/// MY_NUMERIC_IP_ADDR: Ipv4Addr +/// } +/// ``` +/// +/// ## Parse string with custom logic resulting into a number +/// +/// ``` +/// struct CustomConverter(u8); +/// +/// impl From for CustomConverter { +/// fn from(value: String) -> Self { +/// Self(123) +/// } +/// } +/// +/// impl Into for CustomConverter { +/// fn into(self) -> u8 { +/// self.0 +/// } +/// } +/// +/// micronfig::config! { +/// MY_ONETWOTHREE: CustomConverter => u8, +/// MY_ONETWOTHREE_BUT_EXPLICIT: String => CustomConverter => u8 +/// } +/// ``` +/// +#[macro_export] +macro_rules! config { + { $( $identifier:ident: $( $conversion:ty )=>* ),+ } => { + static __micronfig_cache: std::sync::OnceLock<$crate::cache::MicronfigCache> = std::sync::OnceLock::new(); + + fn __micronfig_init_cache() -> $crate::cache::MicronfigCache { + let mut this = $crate::cache::MicronfigCache::default(); + + if cfg!(feature = "envdot") { + this.add_envdot("./.env"); + this.add_envdot("./.env.local"); + } + + this + } + + fn __micronfig_get(key: &str) -> Option { + let mut value: Option = None; + + if cfg!(feature = "envfiles") && value.is_none() { + value = $crate::sources::envfiles::get(format!("{key}_FILE")); + } + + if cfg!(feature = "envvars") && value.is_none() { + value = $crate::sources::envvars::get(&key); + } + + if cfg!(feature = "envdot") && value.is_none() { + let cache = __micronfig_cache.get_or_init(__micronfig_init_cache); + + for dotenv in cache.dotenvs.iter() { + value = $crate::sources::envdot::get(dotenv, &key); + if value.is_some() { + break; + } + } + } + + value + } + + $( + pub(self) mod $identifier { + pub static lock: std::sync::OnceLock> = std::sync::OnceLock::new(); + } + + pub(crate) fn $identifier () -> &'static Option< $crate::__micronfig_last![ $( $conversion, )+ ] > { + $identifier::lock.get_or_init(|| { + let key = stringify!($identifier); + let value = __micronfig_get(key); + + $( + let value: Option<$conversion> = value.map(Into::into); + )+ + + value + }) + } + )+ + } +} + +config! { + SOMETHING: String +} diff --git a/src/sources/envdot.rs b/src/sources/envdot.rs new file mode 100644 index 0000000..e5355c0 --- /dev/null +++ b/src/sources/envdot.rs @@ -0,0 +1,50 @@ +//! Variables defined in specific dotenv files. + +use std::collections::HashMap; +use std::fmt::Debug; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use regex::Regex; + +#[derive(Clone, Default, PartialEq, Eq, Debug)] +pub struct DotEnv( + HashMap +); + +impl DotEnv { + pub fn var(&self, key: &str) -> Option<&String> { + self.0.get(key) + } +} + +impl

From

for DotEnv + where P: AsRef + Debug +{ + fn from(value: P) -> Self { + let mut file = File::open(&value) + .expect(&*format!("to be able to open {value:?}")); + + let mut contents: String = String::new(); + file.read_to_string(&mut contents) + .expect(&*format!("to be able to read {value:?}")); + + let mut keys: HashMap = HashMap::new(); + + let re = Regex::new(r#"^(?:export\s+)?([^=]+)\s*=\s*(.+)$"#) + .expect("Regex to be valid"); + + let _ = contents.split("\n") + .filter_map(|line| re.captures(line)) + .map(|capture| (capture[0].to_owned(), capture[1].to_owned())) + .map(|(key, value)| keys.insert(key, value)); + + Self(keys) + } +} + +/// Get the contents of the file at the path specified by the given environment variable. +pub fn get(dotenv: &DotEnv, key: &str) -> Option +{ + dotenv.var(key).map(|v| v.to_owned()) +} diff --git a/src/sources/envfiles.rs b/src/sources/envfiles.rs new file mode 100644 index 0000000..f390de4 --- /dev/null +++ b/src/sources/envfiles.rs @@ -0,0 +1,21 @@ +//! Contents of files at paths defined by environment variables. + +/// Get the contents of the file at the path specified by the given environment variable. +pub fn get(key: Key) -> Option + where Key: AsRef, +{ + let path = std::env::var(key).ok()?; + + let path = std::ffi::OsString::from(path); + let path = std::path::PathBuf::from(path); + + let mut file = std::fs::File::open(&path) + .expect(&*format!("to be able to open file at {path:?}")); + + use std::io::Read; + let mut data = String::new(); + file.read_to_string(&mut data) + .expect(&*format!("to be able to read from file at {path:?}")); + + Some(data) +} diff --git a/src/sources/envvars.rs b/src/sources/envvars.rs new file mode 100644 index 0000000..330a335 --- /dev/null +++ b/src/sources/envvars.rs @@ -0,0 +1,8 @@ +//! Environment variables. + +/// Get the specified environment variable. +pub fn get(key: Key) -> Option + where Key: AsRef, +{ + std::env::var(key).ok() +} diff --git a/src/sources/mod.rs b/src/sources/mod.rs new file mode 100644 index 0000000..9ef55bd --- /dev/null +++ b/src/sources/mod.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "envvars")] +pub mod envvars; + +#[cfg(feature = "envfiles")] +pub mod envfiles; + +#[cfg(feature = "envdot")] +pub mod envdot;