;
+
+/// 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