diff --git a/.directory b/.directory new file mode 100644 index 0000000..eb708b0 --- /dev/null +++ b/.directory @@ -0,0 +1,2 @@ +[Desktop Entry] +Icon=/home/steffo/Workspaces/Steffo99/micronfig/icon.png diff --git a/.idea/micronfig.iml b/.idea/micronfig_root.iml similarity index 100% rename from .idea/micronfig.iml rename to .idea/micronfig_root.iml diff --git a/.idea/modules.xml b/.idea/modules.xml index 2c3f044..3ceeb74 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,9 @@ - + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8757bd5..2d93727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,5 @@ -[package] -name = "micronfig" -version = "0.2.0" -authors = ["Stefano Pigozzi "] -edition = "2021" -description = "Tiny crate for simple configuration management" -repository = "https://github.com/Steffo99/micronfig/" -license = "MIT OR Apache-2.0" -keywords = ["twelve-factor-app", "configuration", "config", "environment", "envvar"] -categories = ["config"] - - -[package.metadata.docs.rs] -all-features = true -cargo-args = ["--bins"] -rustdoc-args = ["--document-private-items", "--cfg", "docsrs"] - - -[features] -default = ["envvars", "envfiles", "envdot"] -envvars = [] -envfiles = [] -envdot = ["regex"] -testing = ["tempfile"] - -[dependencies] -tempfile = { version = "3.9.0", optional = true } -regex = { version = "1.10.2", optional = true } - +[workspace] +members = [ + "micronfig", + "micronfig_macros", +] diff --git a/Micronfig.iml b/Micronfig.iml new file mode 100644 index 0000000..719cd36 --- /dev/null +++ b/Micronfig.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/_workspace.iml b/_workspace.iml new file mode 100644 index 0000000..c6b9173 --- /dev/null +++ b/_workspace.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/micronfig/Cargo.toml b/micronfig/Cargo.toml new file mode 100644 index 0000000..3e7b5bf --- /dev/null +++ b/micronfig/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "micronfig" +version = "0.3.0" +authors = ["Stefano Pigozzi "] +edition = "2021" +description = "Macro-based configuration management" +repository = "https://github.com/Steffo99/micronfig/" +license = "MIT OR Apache-2.0" +keywords = ["twelve-factor-app", "configuration", "config", "environment", "envvar"] +categories = ["config"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--document-private-items"] + +[features] +default = ["envvars", "envfiles", "envdot"] +envvars = [] +envfiles = [] +envdot = ["regex"] + +[dependencies] +micronfig_macros = { version = "0.3.0", path = "../micronfig_macros" } +regex = { version = "1.10.2", optional = true } diff --git a/micronfig/src/cache.rs b/micronfig/src/cache.rs new file mode 100644 index 0000000..c4bf150 --- /dev/null +++ b/micronfig/src/cache.rs @@ -0,0 +1,65 @@ +//! Definition of [`Cache`]. + +use std::fmt::Debug; + +/// Cache initialized only once per config block and used to quickly retrieve configuration values. +#[derive(Clone, Default, Debug)] +pub struct Cache { + /// `.env` file cache, in order of access priority. + /// + /// More can be added with [`Cache::register_dotenv`]. + #[cfg(feature = "envdot")] + pub envdot: Vec +} + +impl Cache { + /// Initialize a new cache. + pub fn new() -> Self { + let mut this = Self::default(); + + if cfg!(feature = "envdot") { + this.register_dotenv("./.env.local"); + this.register_dotenv("./.env"); + } + + this + } + + /// Get a value from the cache. + pub fn get(&self, key: Key) -> Option + where Key: AsRef<&std::ffi::OsStr> + { + let mut value = None; + + if cfg!(feature = "envfiles") { + value = crate::envfiles::get(format!("{key}_FILE")); + } + + if cfg!(feature = "envvars") && value.is_none() { + value = crate::envvars::get(&key); + } + + if cfg!(feature = "envdot") && value.is_none() { + for dotenv in self.envdot.iter() { + value = crate::envdot::get(dotenv, &key); + if value.is_some() { + break; + } + } + } + + value + } + + /// Register a new `.env` file in the cache. + /// + /// Equivalent to adding an item to [`Cache::envdot`]. + #[cfg(feature = "envdot")] + pub fn register_dotenv(&mut self, path: Path) + where Path: AsRef + Debug + { + self.envdot.push( + crate::envdot::DotEnv::from(path) + ); + } +} diff --git a/micronfig/src/envdot.rs b/micronfig/src/envdot.rs new file mode 100644 index 0000000..6b3271b --- /dev/null +++ b/micronfig/src/envdot.rs @@ -0,0 +1,79 @@ +//! Utilities for fetching configuration values defined in specific `.env` files. + +use std::collections::HashMap; +use std::ffi::OsString; +use std::fmt::Debug; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use regex::Regex; + +/// The type of a parsed `.env` file. +pub type DotEnv = HashMap; + +/// Parse a `.env` file. +/// +/// ### Warning +/// +/// This method isn't properly +fn parse_dotenv

(value: P) -> DotEnv + where P: AsRef + Debug +{ + 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| { + let key = &capture[0]; + let value = &capture[1]; + + if value.starts_with('\'') && value.ends_with('\'') { + ( + key.into(), + value + .strip_prefix('\'') + .expect("apostrophe to be prefixed to the value") + .strip_suffix('\'') + .expect("apostrophe to be suffixed to the value") + .to_owned() + ) + } + else if value.starts_with('"') && value.ends_with('"') { + ( + key.into(), + value + .strip_prefix('"') + .expect("quotes to be prefixed to the value") + .strip_suffix('"') + .expect("quotes to be suffixed to the value") + .to_owned() + ) + } + else { + ( + key.into(), + value.to_owned() + ) + } + }) + .map(|(key, value)| keys.insert(key, value)); + + keys +} + +/// Get the requested variable from a [`DotEnv`] structure. +pub fn get(dotenv: &DotEnv, key: Key) -> Option + where Key: AsRef<&std::ffi::OsStr> +{ + dotenv.var(key).map(|v| v.to_owned()) +} diff --git a/src/sources/envfiles.rs b/micronfig/src/envfiles.rs similarity index 72% rename from src/sources/envfiles.rs rename to micronfig/src/envfiles.rs index f390de4..5509dcb 100644 --- a/src/sources/envfiles.rs +++ b/micronfig/src/envfiles.rs @@ -1,10 +1,12 @@ //! Contents of files at paths defined by environment variables. -/// Get the contents of the file at the path specified by the given environment variable. +use std::io::Read; + +/// Get the contents of the file at the path specified by the requested environment variable plus `_FILE`. pub fn get(key: Key) -> Option where Key: AsRef, { - let path = std::env::var(key).ok()?; + let path = std::env::var(format!("{key}_FILE")).ok()?; let path = std::ffi::OsString::from(path); let path = std::path::PathBuf::from(path); @@ -12,7 +14,6 @@ pub fn get(key: Key) -> Option 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:?}")); diff --git a/src/sources/envvars.rs b/micronfig/src/envvars.rs similarity index 100% rename from src/sources/envvars.rs rename to micronfig/src/envvars.rs diff --git a/src/sources/mod.rs b/micronfig/src/lib.rs similarity index 90% rename from src/sources/mod.rs rename to micronfig/src/lib.rs index 9ef55bd..328d9b0 100644 --- a/src/sources/mod.rs +++ b/micronfig/src/lib.rs @@ -1,8 +1,8 @@ +pub mod cache; + #[cfg(feature = "envvars")] pub mod envvars; - #[cfg(feature = "envfiles")] pub mod envfiles; - #[cfg(feature = "envdot")] pub mod envdot; diff --git a/micronfig_macros/Cargo.toml b/micronfig_macros/Cargo.toml new file mode 100644 index 0000000..656dfc6 --- /dev/null +++ b/micronfig_macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "micronfig_macros" +version = "0.3.0" +authors = ["Stefano Pigozzi "] +edition = "2021" +description = "Macros for micronfig" +repository = "https://github.com/Steffo99/micronfig/" +license = "MIT OR Apache-2.0" +keywords = ["twelve-factor-app", "configuration", "config", "environment", "envvar"] +categories = ["config"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--document-private-items"] + +[dependencies] +syn = "2.0" +quote = "1.0" + +[lib] +proc-macro = true diff --git a/micronfig_macros/micronfig_macros.iml b/micronfig_macros/micronfig_macros.iml new file mode 100644 index 0000000..c981aff --- /dev/null +++ b/micronfig_macros/micronfig_macros.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/micronfig_macros/src/lib.rs b/micronfig_macros/src/lib.rs new file mode 100644 index 0000000..88d2155 --- /dev/null +++ b/micronfig_macros/src/lib.rs @@ -0,0 +1,129 @@ +extern crate proc_macro; +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, parse_macro_input, Token, Type}; +use syn::punctuated::{Pair, Punctuated}; + + +type Config = Punctuated; + +struct ConfigItem { + identifier: Ident, + types: ConfigTypes, +} + +type ConfigTypes = Punctuated; + +enum Conversion { + From, + TryFrom, + FromStr, +} + + +impl Parse for ConfigItem { + fn parse(input: ParseStream) -> syn::Result { + let identifier = input.parse::()?; + let types = input.parse::>()?; + Ok(Self { identifier, types }) + } +} + +impl Parse for Conversion { + fn parse(input: ParseStream) -> syn::Result { + if input.parse::]>().is_ok() { + Ok(Conversion::From) + } + else if input.parse::]>().is_ok() { + Ok(Conversion::TryFrom) + } + else if input.parse::]>().is_ok() { + Ok(Conversion::FromStr) + } + else { + Err(input.error("Cannot determine conversion method to use; valid conversion tokens are `->` (From), `=>` (TryFrom) and `>` (FromStr).")) + } + } +} + +#[proc_macro] +pub fn config(input: TokenStream) -> TokenStream { + let input: Config = parse_macro_input!(input as Config); + + let cache = quote! { + mod _cache { + pub static lock: std::sync::OnceLock = std::sync::OnceLock::new(); + } + + pub(self) fn _cache() { + _cache::lock.get_or_init(micronfig::cache::Cache::new) + } + }; + + let items = input.iter().map(|item: ConfigItem| { + let identifier = item.identifier; + + // TODO: Can types be zero-length? + + let mut conversion_code = quote! {}; + + let mut previous_conversion: Option<&Conversion> = None; + + for pair in item.types.pairs().into_iter() { + if let Some(some_conversion) = previous_conversion { + todo!(); + + match pair { + Pair::Punctuated(target_type, new_conversion) => { + previous_conversion = Some(new_conversion); + } + Pair::End(target_type) => { + + } + } + + conversion_code = match some_conversion { + Conversion::From => quote! { + #conversion_code + let value: #target_type = value.into(); + } + Conversion::TryFrom => quote! { + #conversion_code + let value: #target_type = value.try_into() + .expect("to be able to convert {}", stringify!(#identifier)); + } + Conversion::FromStr => quote! { + #conversion_code + let value: #target_type = value.parse() + .expect("to be able to parse {}", stringify!(#identifier)); + } + }; + } + }; + + let last_type = item.types.last(); + + quote! { + mod #identifier { + pub(super) lock: std::sync::OnceLock<#last_type> = std::sync::OnceLock::new(); + } + + pub(crate) fn #identifier() { + #identifier::lock.get_or_init(|| { + let key = stringify!(#identifier); + let value = _cache().get(&key); + + #conversion_code + + value + }) + } + } + }); + + quote! { + #cache + #items + } +} \ No newline at end of file diff --git a/src/cache.rs b/src/cache.rs deleted file mode 100644 index 7e9a127..0000000 --- a/src/cache.rs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 231658e..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,143 +0,0 @@ -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 deleted file mode 100644 index e5355c0..0000000 --- a/src/sources/envdot.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! 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()) -}