1
Fork 0
mirror of https://github.com/Steffo99/micronfig.git synced 2024-10-16 14:37:29 +00:00

so much stuff...

This commit is contained in:
Steffo 2024-01-02 06:41:44 +01:00
parent ef0094cd2c
commit a6f08368b4
Signed by: steffo
GPG key ID: 2A24051445686895
18 changed files with 368 additions and 250 deletions

2
.directory Normal file
View file

@ -0,0 +1,2 @@
[Desktop Entry]
Icon=/home/steffo/Workspaces/Steffo99/micronfig/icon.png

View file

@ -2,7 +2,9 @@
<project version="4">
<component name="ProjectModuleManager">
<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>
</component>
</project>

View file

@ -1,29 +1,5 @@
[package]
name = "micronfig"
version = "0.2.0"
authors = ["Stefano Pigozzi <me@steffo.eu>"]
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",
]

9
Micronfig.iml Normal file
View 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
View 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
View 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
View 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
View 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())
}

View file

@ -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: Key) -> Option<String>
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::path::PathBuf::from(path);
@ -12,7 +14,6 @@ pub fn get<Key>(key: Key) -> Option<String>
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:?}"));

View file

@ -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;

View 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

View 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
View 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
}
}

View file

@ -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
)
);
}
}

View file

@ -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
}

View file

@ -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())
}