mirror of
https://github.com/Steffo99/micronfig.git
synced 2024-11-22 08:04:20 +00:00
so much stuff...
This commit is contained in:
parent
ef0094cd2c
commit
a6f08368b4
18 changed files with 368 additions and 250 deletions
2
.directory
Normal file
2
.directory
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Icon=/home/steffo/Workspaces/Steffo99/micronfig/icon.png
|
|
@ -2,7 +2,9 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/micronfig.iml" filepath="$PROJECT_DIR$/.idea/micronfig.iml" />
|
<module fileurl="file://$PROJECT_DIR$/_workspace.iml" filepath="$PROJECT_DIR$/_workspace.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/micronfig/micronfig.iml" filepath="$PROJECT_DIR$/micronfig/micronfig.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/micronfig_macros/micronfig_macros.iml" filepath="$PROJECT_DIR$/micronfig_macros/micronfig_macros.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
34
Cargo.toml
34
Cargo.toml
|
@ -1,29 +1,5 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "micronfig"
|
members = [
|
||||||
version = "0.2.0"
|
"micronfig",
|
||||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
"micronfig_macros",
|
||||||
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 }
|
|
||||||
|
|
||||||
|
|
9
Micronfig.iml
Normal file
9
Micronfig.iml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="RUST_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
11
_workspace.iml
Normal file
11
_workspace.iml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="RUST_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
24
micronfig/Cargo.toml
Normal file
24
micronfig/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "micronfig"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||||
|
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 }
|
65
micronfig/src/cache.rs
Normal file
65
micronfig/src/cache.rs
Normal file
|
@ -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<crate::envdot::DotEnv>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Key>(&self, key: Key) -> Option<String>
|
||||||
|
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<Path>(&mut self, path: Path)
|
||||||
|
where Path: AsRef<std::path::Path> + Debug
|
||||||
|
{
|
||||||
|
self.envdot.push(
|
||||||
|
crate::envdot::DotEnv::from(path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
79
micronfig/src/envdot.rs
Normal file
79
micronfig/src/envdot.rs
Normal file
|
@ -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<OsString, String>;
|
||||||
|
|
||||||
|
/// Parse a `.env` file.
|
||||||
|
///
|
||||||
|
/// ### Warning
|
||||||
|
///
|
||||||
|
/// This method isn't properly
|
||||||
|
fn parse_dotenv<P>(value: P) -> DotEnv
|
||||||
|
where P: AsRef<Path> + 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<OsString, String> = 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<Key>(dotenv: &DotEnv, key: Key) -> Option<String>
|
||||||
|
where Key: AsRef<&std::ffi::OsStr>
|
||||||
|
{
|
||||||
|
dotenv.var(key).map(|v| v.to_owned())
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
//! Contents of files at paths defined by environment variables.
|
//! 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: Key) -> Option<String>
|
pub fn get<Key>(key: Key) -> Option<String>
|
||||||
where Key: AsRef<std::ffi::OsStr>,
|
where Key: AsRef<std::ffi::OsStr>,
|
||||||
{
|
{
|
||||||
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::ffi::OsString::from(path);
|
||||||
let path = std::path::PathBuf::from(path);
|
let path = std::path::PathBuf::from(path);
|
||||||
|
@ -12,7 +14,6 @@ pub fn get<Key>(key: Key) -> Option<String>
|
||||||
let mut file = std::fs::File::open(&path)
|
let mut file = std::fs::File::open(&path)
|
||||||
.expect(&*format!("to be able to open file at {path:?}"));
|
.expect(&*format!("to be able to open file at {path:?}"));
|
||||||
|
|
||||||
use std::io::Read;
|
|
||||||
let mut data = String::new();
|
let mut data = String::new();
|
||||||
file.read_to_string(&mut data)
|
file.read_to_string(&mut data)
|
||||||
.expect(&*format!("to be able to read from file at {path:?}"));
|
.expect(&*format!("to be able to read from file at {path:?}"));
|
|
@ -1,8 +1,8 @@
|
||||||
|
pub mod cache;
|
||||||
|
|
||||||
#[cfg(feature = "envvars")]
|
#[cfg(feature = "envvars")]
|
||||||
pub mod envvars;
|
pub mod envvars;
|
||||||
|
|
||||||
#[cfg(feature = "envfiles")]
|
#[cfg(feature = "envfiles")]
|
||||||
pub mod envfiles;
|
pub mod envfiles;
|
||||||
|
|
||||||
#[cfg(feature = "envdot")]
|
#[cfg(feature = "envdot")]
|
||||||
pub mod envdot;
|
pub mod envdot;
|
21
micronfig_macros/Cargo.toml
Normal file
21
micronfig_macros/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "micronfig_macros"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||||
|
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
|
14
micronfig_macros/micronfig_macros.iml
Normal file
14
micronfig_macros/micronfig_macros.iml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="RUST_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
129
micronfig_macros/src/lib.rs
Normal file
129
micronfig_macros/src/lib.rs
Normal file
|
@ -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<Config, Token![,]>;
|
||||||
|
|
||||||
|
struct ConfigItem {
|
||||||
|
identifier: Ident,
|
||||||
|
types: ConfigTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigTypes = Punctuated<Type, Conversion>;
|
||||||
|
|
||||||
|
enum Conversion {
|
||||||
|
From,
|
||||||
|
TryFrom,
|
||||||
|
FromStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Parse for ConfigItem {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let identifier = input.parse::<Ident>()?;
|
||||||
|
let types = input.parse::<Punctuated<Type, Conversion>>()?;
|
||||||
|
Ok(Self { identifier, types })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Conversion {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
if input.parse::<Token![->]>().is_ok() {
|
||||||
|
Ok(Conversion::From)
|
||||||
|
}
|
||||||
|
else if input.parse::<Token![=>]>().is_ok() {
|
||||||
|
Ok(Conversion::TryFrom)
|
||||||
|
}
|
||||||
|
else if input.parse::<Token![>]>().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<Cache> = 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
|
||||||
|
}
|
||||||
|
}
|
22
src/cache.rs
22
src/cache.rs
|
@ -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<crate::sources::envdot::DotEnv>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MicronfigCache {
|
|
||||||
#[cfg(feature = "envdot")]
|
|
||||||
pub fn add_envdot<P>(&mut self, path: P)
|
|
||||||
where P: AsRef<Path> + Debug
|
|
||||||
{
|
|
||||||
self.dotenvs.push(
|
|
||||||
crate::sources::envdot::DotEnv::from(
|
|
||||||
path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
143
src/lib.rs
143
src/lib.rs
|
@ -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<String> for CustomConverter {
|
|
||||||
/// fn from(value: String) -> Self {
|
|
||||||
/// Self(123)
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// impl Into<u8> 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<String> {
|
|
||||||
let mut value: Option<String> = 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<Option< $crate::__micronfig_last![ $( $conversion, )+ ] >> = 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
|
|
||||||
}
|
|
|
@ -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<String, String>
|
|
||||||
);
|
|
||||||
|
|
||||||
impl DotEnv {
|
|
||||||
pub fn var(&self, key: &str) -> Option<&String> {
|
|
||||||
self.0.get(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P> From<P> for DotEnv
|
|
||||||
where P: AsRef<Path> + 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<String, String> = 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<String>
|
|
||||||
{
|
|
||||||
dotenv.var(key).map(|v| v.to_owned())
|
|
||||||
}
|
|
Loading…
Reference in a new issue