1
Fork 0
mirror of https://github.com/Steffo99/micronfig.git synced 2024-10-16 06:27:28 +00:00

Rewrite library with procedural macros (#1)

This commit is contained in:
Steffo 2024-01-05 07:48:51 +01:00 committed by GitHub
parent 825a20b19a
commit 4c1bac2857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 1592 additions and 1102 deletions

2
.directory Normal file
View file

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

View file

@ -2,6 +2,8 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RsFunctionNaming" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="RsModuleNaming" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="SillyAssignmentJS" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" /> <inspection_tool class="SillyAssignmentJS" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />

View file

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

View file

@ -1,31 +1,6 @@
[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" resolver = "2"
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 = ["single_envvars", "single_envfiles", "multi", "handle", "macros"]
single_envvars = []
single_envfiles = []
multi = ["single_envvars", "single_envfiles"]
handle = ["multi"]
macros = ["lazy_static", "handle"]
testing = ["tempfile"]
[dependencies]
lazy_static = { version = "1.4.0", optional = true }
tempfile = { version = "3.5.0", optional = true }

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>

13
_workspace.iml Normal file
View file

@ -0,0 +1,13 @@
<?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$/micronfig/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/micronfig/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -1,8 +0,0 @@
[package]
name = "e01_the_cave"
description = "Echoes back the value of ECHO."
version = "0.0.0"
edition = "2021"
[dependencies]
micronfig = { path = "../.." }

View file

@ -1,9 +0,0 @@
use std::fmt::Display;
micronfig::required!(ECHO, String);
fn main() {
println!("{}", *ECHO);
}

View file

@ -1,8 +0,0 @@
[package]
name = "e02_quick_math"
description = "Performs OPERATOR between FIRST and SECOND."
version = "0.0.0"
edition = "2021"
[dependencies]
micronfig = { path = "../.." }

View file

@ -1,52 +0,0 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
micronfig::required!(FIRST, u64);
micronfig::required!(SECOND, u64);
micronfig::required!(OPERATOR, Operator);
fn main() {
let result = match *OPERATOR {
Operator::Sum => (*FIRST) + (*SECOND),
Operator::Subtraction => (*FIRST) - (*SECOND),
Operator::Multiplication => (*FIRST) * (*SECOND),
Operator::Division => (*FIRST) / (*SECOND),
};
println!("{} {} {} = {}", *FIRST, *OPERATOR, *SECOND, result)
}
pub enum Operator {
Sum,
Subtraction,
Multiplication,
Division,
}
impl FromStr for Operator {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"+" => Ok(Self::Sum),
"-" => Ok(Self::Subtraction),
"*" => Ok(Self::Multiplication),
"/" => Ok(Self::Division),
_ => Err(())
}
}
}
impl Display for Operator {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Sum => "+",
Self::Subtraction => "-",
Self::Multiplication => "*",
Self::Division => "/",
})
}
}

View file

@ -1,8 +0,0 @@
[package]
name = "e03_order_a_pizza"
description = "Order a pizza using micronfig!"
version = "0.0.0"
edition = "2021"
[dependencies]
micronfig = { path = "../.." }

View file

@ -1,119 +0,0 @@
use std::fmt::Formatter;
use std::net::IpAddr;
use std::str::FromStr;
// The name of the person who ordered the pizza.
micronfig::required!(FULLNAME, String);
// The (IP) address the pizza should be delivered to.
micronfig::required!(DESTINATION, IpAddr);
// The base of the pizza to add toppings on.
micronfig::required!(PIZZABASE, PizzaBase);
// The toppings to add to the pizza.
micronfig::optional!(PIZZATOPPINGS, PizzaToppingsList);
// A pizza with no toppings, to use as fallback.
const PIZZATOPPINGS_NONE: PizzaToppingsList = PizzaToppingsList{ list: vec![] };
fn main() {
// Let's print the order!
println!("Pizza Order");
println!("===========");
println!();
println!("Base:");
println!("- {}", *PIZZABASE);
println!();
println!("Toppings:");
for topping in &(*PIZZATOPPINGS).as_ref().unwrap_or(&PIZZATOPPINGS_NONE).list {
println!("- {}", &topping);
};
println!();
println!("Deliver to:");
println!("{} @ {}", *FULLNAME, *DESTINATION)
}
/// A possible base of pizza.
#[derive(Clone, Copy, Debug)]
enum PizzaBase {
/// Just the pizza dough, with nothing else on top f it.
Blank,
/// Pizza dough with tomato on top.
Red,
/// Pizza dough with mozzarella on top.
White,
/// Pizza dough with both tomato and mozzarella on top.
Margherita,
}
impl FromStr for PizzaBase {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
// Italian
"vuota" => Ok(Self::Blank),
"stria" => Ok(Self::Blank),
"rossa" => Ok(Self::Red),
"marinara" => Ok(Self::Red),
"pomodoro" => Ok(Self::Red),
"bianca" => Ok(Self::White),
"mozzarella" => Ok(Self::White),
"regina" => Ok(Self::Margherita),
"margherita" => Ok(Self::Margherita),
"normale" => Ok(Self::Margherita),
"entrambi" => Ok(Self::Margherita),
// English
"blank" => Ok(Self::Blank),
"red" => Ok(Self::Red),
"tomato" => Ok(Self::Red),
"white" => Ok(Self::White),
"cheese" => Ok(Self::White),
"both" => Ok(Self::Margherita),
"normal" => Ok(Self::Margherita),
// Unknown
_ => Err("Unknown pizza base; ensure you have written the name in either English or Italian!"),
}
}
}
impl std::fmt::Display for PizzaBase {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
PizzaBase::Blank => "Blank (Empty)",
PizzaBase::Red => "Red (Tomato)",
PizzaBase::White => "White (Mozzarella)",
PizzaBase::Margherita => "Margherita (Tomato + Mozzarella)"
})
}
}
/// The toppings
#[derive(Clone, Debug)]
struct PizzaToppingsList {
pub list: Vec<String>
}
impl FromStr for PizzaToppingsList {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let list: Vec<String> = s.split(",").map(|s| s.to_string()).collect();
for topping in list.iter() {
// Ensure compatibility with https://github.com/rust-lang/rust/pull/70645
if ["pineapple", "ananas"].contains(&topping.as_str()) {
return Err("Ruining pizzas is not allowed by the Rust compiler.")
}
}
Ok(
PizzaToppingsList {
list
}
)
}
}

26
micronfig/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[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
[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 }
[dev-dependencies]
tempfile = { version = "3.9.0" }

212
micronfig/src/cache.rs Normal file
View file

@ -0,0 +1,212 @@
//! **Private**; definition of [`Cache`].
use std::ffi::OsStr;
use std::fmt::Debug;
/// Cache initialized only once per config block and used to quickly retrieve configuration values.
///
/// Every `env*` feature has its own field here, which may or may not be used.
#[derive(Clone, Default, Debug)]
pub struct Cache {
/// Unused.
#[cfg(feature = "envvars")]
pub envvars: (),
/// Unused.
#[cfg(feature = "envfiles")]
pub envfiles: (),
/// `.env` file cache, in order of access priority.
///
/// More can be added with [`Cache::envdot_register`].
#[cfg(feature = "envdot")]
pub envdot: Vec<crate::envdot::DotEnv>,
}
impl Cache {
/// Initialize a new cache.
pub fn new() -> Self {
let mut this = Self::default();
this.init_envfiles();
this.init_envvars();
this.init_envdot();
this
}
#[cfg(feature = "envfiles")]
fn init_envfiles(&mut self) {}
#[cfg(not(feature = "envfiles"))]
fn init_envfiles(&mut self) {}
#[cfg(feature = "envvars")]
fn init_envvars(&mut self) {}
#[cfg(not(feature = "envvars"))]
fn init_envvars(&mut self) {}
#[cfg(feature = "envdot")]
fn init_envdot(&mut self) {
self.envdot_register("./.env.local");
self.envdot_register("./.env");
}
#[cfg(not(feature = "envdot"))]
fn init_envdot(&mut self) {}
/// Register a new `.env` file in the cache, if it exists.
#[cfg(feature = "envdot")]
pub fn envdot_register<Path>(&mut self, path: Path)
where Path: AsRef<std::path::Path> + Debug
{
let dotenv = crate::envdot::parse_dotenv(path);
if let Some(dotenv) = dotenv {
self.envdot.push(dotenv);
}
}
/// Get a value from the cache.
///
/// The following sources, if the respective feature is enabled, are checked in the following order:
/// 1. `envfiles`
/// 2. `envvars`
/// 3. `envdot`
///
pub fn get(&self, key: &OsStr) -> Option<String>
{
let mut value = None;
if value.is_none() { value = self.get_from_envfiles(key); }
if value.is_none() { value = self.get_from_envvars(key); }
if value.is_none() { value = self.get_from_envdot(key); }
value
}
#[cfg(feature = "envfiles")]
pub fn get_from_envfiles(&self, key: &OsStr) -> Option<String> {
crate::envfiles::get(key)
}
#[cfg(not(feature = "envfiles"))]
pub fn get_from_envfiles(&self, _key: &OsStr) -> Option<String> {
None
}
#[cfg(feature = "envvars")]
pub fn get_from_envvars(&self, key: &OsStr) -> Option<String> {
crate::envvars::get(key)
}
#[cfg(not(feature = "envvars"))]
pub fn get_from_envvars(&self, _key: &OsStr) -> Option<String> {
None
}
#[cfg(feature = "envdot")]
pub fn get_from_envdot(&self, key: &OsStr) -> Option<String> {
for dotenv in self.envdot.iter() {
let value = crate::envdot::get(dotenv, key);
if value.is_some() {
return value
}
}
None
}
#[cfg(not(feature = "envdot"))]
pub fn get_from_envdot(&self, _key: &OsStr) -> Option<String> {
None
}
}
//noinspection DotEnvSpaceAroundSeparatorInspection
#[cfg(test)]
mod tests {
use crate::testing::tempfile_fixture;
use super::*;
#[cfg(feature = "envdot")]
#[test]
fn envdot_register() {
let file = tempfile_fixture(
// language=dotenv
r#"
GARAS=garas
export AUTO= auto
BUS = bus
"#
);
let mut cache = Cache::default();
cache.envdot_register(file.as_os_str());
assert_eq!(cache.envdot.len(), 1);
}
#[cfg(feature = "envvars")]
#[test]
fn get_envvars() {
std::env::set_var("GARAS", "garas");
std::env::remove_var("GARAS_FILE");
let cache = Cache::default();
assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string()));
}
#[cfg(feature = "envfiles")]
#[test]
fn get_envfiles() {
let file = tempfile_fixture("garas");
std::env::remove_var("GARAS");
std::env::set_var("GARAS_FILE", file.as_os_str());
let cache = Cache::default();
assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string()));
}
#[cfg(feature = "envdot")]
#[test]
fn get_envdot() {
std::env::remove_var("GARAS");
std::env::remove_var("GARAS_FILE");
let file = tempfile_fixture(
// language=dotenv
r#"GARAS=garas"#
);
let mut cache = Cache::default();
cache.envdot_register(file.as_os_str());
assert_eq!(cache.get("GARAS".as_ref()), Some("garas".to_string()));
}
#[test]
#[cfg(all(feature = "envdot", feature = "envfiles", feature = "envvars"))]
fn priority() {
let mut cache = Cache::default();
let envfiles_file = tempfile_fixture("envfiles");
let envdot_file = tempfile_fixture(
// language=dotenv
r#"
export ENVFILES=envdot
export ENVVARS=envdot
export ENVDOT=envdot
"#
);
std::env::set_var("ENVFILES_FILE", envfiles_file.as_os_str());
std::env::remove_var("ENVVARS_FILE");
std::env::remove_var("ENVDOT_FILE");
std::env::remove_var("NONE_FILE");
std::env::set_var("ENVFILES", "envvars");
std::env::set_var("ENVVARS", "envvars");
std::env::remove_var("ENVDOT");
std::env::remove_var("NONE");
cache.envdot_register(envdot_file.as_os_str());
assert_eq!(cache.get("ENVFILES".as_ref()), Some("envfiles".to_string()));
assert_eq!(cache.get("ENVVARS".as_ref()), Some("envvars".to_string()));
assert_eq!(cache.get("ENVDOT".as_ref()), Some("envdot".to_string()));
assert_eq!(cache.get("NONE".as_ref()), None);
}
}

168
micronfig/src/envdot.rs Normal file
View file

@ -0,0 +1,168 @@
//! **Private**; utilities for fetching configuration values defined in specific `.env` files.
use std::collections::HashMap;
use std::ffi::{OsStr, 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.
///
/// Returns [`None`] if no such file is found.
pub fn parse_dotenv<P>(value: P) -> Option<DotEnv>
where P: AsRef<Path> + Debug
{
let mut file = File::open(&value).ok()?;
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#"^\s*(?:export\s)?\s*([^=]+?)\s*=\s*(.+)\s*$"#)
.expect("Regex to be valid");
contents.split("\n")
.filter_map(|line| re.captures(line))
.map(|capture| {
let key = &capture[1];
let value = &capture[2];
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()
)
}
})
.for_each(|(key, value)| {
keys.insert(key, value);
});
Some(keys)
}
/// Get the requested variable from a [`DotEnv`] structure.
pub fn get(dotenv: &DotEnv, key: &OsStr) -> Option<String>
{
dotenv.get(key).map(|v| v.to_owned())
}
//noinspection DotEnvSpaceAroundSeparatorInspection
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::tempfile_fixture;
#[test]
fn dotenv_simple() {
let file = tempfile_fixture(
// language=dotenv
r#"
GARAS=garas
AUTO= auto
BUS = bus
"#
);
let parsed = parse_dotenv(file);
let mut compared: HashMap<OsString, String> = HashMap::new();
compared.insert("GARAS".into(), "garas".into());
compared.insert("AUTO".into(), "auto".into());
compared.insert("BUS".into(), "bus".into());
assert_eq!(parsed, Some(compared));
}
#[test]
fn dotenv_apos() {
let file = tempfile_fixture(
// language=dotenv
r#"
GARAS='garas'
AUTO= 'auto'
BUS = 'bus'
"#
);
let parsed = parse_dotenv(file);
let mut compared: HashMap<OsString, String> = HashMap::new();
compared.insert("GARAS".into(), "garas".into());
compared.insert("AUTO".into(), "auto".into());
compared.insert("BUS".into(), "bus".into());
assert_eq!(parsed, Some(compared));
}
#[test]
fn dotenv_quote() {
let file = tempfile_fixture(
// language=dotenv
r#"
GARAS="garas"
AUTO= "auto"
BUS = "bus"
"#
);
let parsed = parse_dotenv(file);
let mut compared: HashMap<OsString, String> = HashMap::new();
compared.insert("GARAS".into(), "garas".into());
compared.insert("AUTO".into(), "auto".into());
compared.insert("BUS".into(), "bus".into());
assert_eq!(parsed, Some(compared));
}
#[test]
fn dotenv_export() {
let file = tempfile_fixture(
// language=dotenv
r#"
export GARAS=garas
export AUTO= auto
export BUS = bus
"#
);
let parsed = parse_dotenv(file);
let mut compared: HashMap<OsString, String> = HashMap::new();
compared.insert("GARAS".into(), "garas".into());
compared.insert("AUTO".into(), "auto".into());
compared.insert("BUS".into(), "bus".into());
assert_eq!(parsed, Some(compared));
}
}

53
micronfig/src/envfiles.rs Normal file
View file

@ -0,0 +1,53 @@
//! **Private**; utilities for fetching configuration values from contents of files at paths defined by environment variables.
use std::ffi::OsStr;
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: &OsStr) -> Option<String> {
let mut key: std::ffi::OsString = key.to_os_string();
key.push("_FILE");
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:?}"));
let mut data = String::new();
file.read_to_string(&mut data)
.expect(&*format!("to be able to read from file at {path:?}"));
Some(data)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::testing::tempfile_fixture;
#[test]
fn it_works() {
let file = tempfile_fixture("XYZ");
std::env::set_var("LETTERS_FILE", file.as_os_str());
let value = get("LETTERS".as_ref());
assert_eq!(value, Some("XYZ".to_string()));
}
#[test]
fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST_FILE");
let value = get("THIS_ENVVAR_DOES_NOT_EXIST".as_ref());
assert_eq!(value, None)
}
#[test]
#[should_panic]
fn missing_file() {
std::env::set_var("NONEXISTENT_FILE", "/this/file/does/not/exist");
let value = get("NONEXISTENT".as_ref());
println!("{:?}", value);
}
}

27
micronfig/src/envvars.rs Normal file
View file

@ -0,0 +1,27 @@
//! **Private**; utilities for fetching configuration values from environment variables.
use std::ffi::OsStr;
/// Get the specified environment variable.
pub fn get(key: &OsStr) -> Option<String> {
std::env::var(key).ok()
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[test]
fn it_works() {
std::env::set_var("LETTERS", "XYZ");
let value = get("LETTERS".as_ref());
assert_eq!(value, Some("XYZ".to_string()));
}
#[test]
fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST");
let value = get("THIS_ENVVAR_DOES_NOT_EXIST".as_ref());
assert_eq!(value, None);
}
}

194
micronfig/src/lib.rs Normal file
View file

@ -0,0 +1,194 @@
//! Crate for macro-based configuration management.
//!
//! ## Description
//!
//! This crate and it's sister [`micronfig_macros`] combine to provide the [`config`] macro, which allows the developer to define all configuration variables required by an application in a single place and have them expanded to static references of the desired types.
//!
//! ```
//! micronfig::config! {
//! DATABASE_URI,
//! APPLICATION_NAME: String,
//! MAX_CONCURRENT_USERS: String > u64,
//! SHOWN_ALERT?,
//! }
//! ```
//!
//! ## Examples
//!
//! ### Strings configuration
//!
//! To define configuration variables returning a string, create a [`config`] block and list their names separated by commas `,`:
//!
//! ```
//! micronfig::config! {
//! VARIABLE_A,
//! VARIABLE_B,
//! VARIABLE_C,
//! PATH,
//! }
//! ```
//!
//! To access them, call their name as if it was a function:
//!
//! ```
//! # micronfig::config! {
//! # VARIABLE_A,
//! # VARIABLE_B,
//! # VARIABLE_C,
//! # PATH,
//! # }
//! #
//! # std::env::set_var("VARIABLE_A", "a");
//! # std::env::set_var("VARIABLE_B", "b");
//! # std::env::set_var("VARIABLE_C", "c");
//! # std::env::set_var("PATH", "/bin");
//! #
//! # if cfg!(feature = "envvars") {
//! // These will all return `&'static str` values.
//! println!("{}", VARIABLE_A());
//! println!("{}", VARIABLE_B());
//! println!("{}", VARIABLE_C());
//! println!("{}", PATH());
//! # }
//! ```
//!
//! > ***Note***
//! >
//! > Both the [`config`] block and variables defined in it are lazily initialized on first call.
//! >
//! > The first time one of these functions is called, configuration files will be parsed, and the first time each is called, its value is retrieved and stored.
//!
//! ### Required and optional variables
//!
//! By default, configuration variables are all required, causing a [panic] if their value is missing the first time their function is called.
//!
//! Configuration variables can be marked as [Option]al by suffixing a question mark `?` to their name, making them return a `&'static` [`Option`] instead:
//!
//! ```
//! micronfig::config! {
//! VARIABLE_REQUIRED,
//! VARIABLE_OPTIONAL?,
//! }
//! ```
//!
//! ### Conversions
//!
//! All variables are read from their source as strings; therefore, the following explicit syntax for defining them is supported:
//!
//! ```
//! micronfig::config! {
//! VARIABLE_A: String,
//! VARIABLE_B: String,
//! VARIABLE_C: String,
//! }
//! ```
//!
//! Strings are not the best option for most situations, so the crate makes use of some traits to allow their conversion to different types:
//!
//! | Trait | Symbol | Notes |
//! |---|---|---|
//! | [`From`] | `->` | |
//! | [`TryFrom`] | `=>` | Will panic if the conversion fails. |
//! | [`std::str::FromStr`] | `>` | Will panic if the parsing fails. |
//!
//! The syntax for conversion is as follows:
//!
//! ```
//! micronfig::config! {
//! // use FromStr to parse the String as an isize
//! REQUIRED_SIGNED: String > isize,
//! // use FromStr to parse the String as a SocketAddr
//! REQUIRED_SOCKETADDR: String > std::net::SocketAddr,
//! // use From to convert the String to... another String
//! REQUIRED_STRING: String -> String,
//! // use TryFrom to convert the String to another String
//! // (there aren't many types in std to make valid examples from!)
//! REQUIRED_TRYSTRING: String => String,
//! // the conversion will not be performed for missing optional variables
//! OPTIONAL_UNSIGNED?: String > usize,
//! }
//! ```
//!
//! > ***Warning***
//! >
//! > Types should always be fully qualified, or the macro won't work properly!
//!
//! Custom types can be used as well:
//!
//! ```
//! struct Duplicator {
//! copy_a: String,
//! copy_b: String,
//! }
//!
//! impl From<String> for Duplicator {
//! fn from(value: String) -> Self {
//! Self {
//! copy_a: value.clone(),
//! copy_b: value
//! }
//! }
//! }
//!
//! micronfig::config! {
//! MY_CUSTOM_TYPE: String -> crate::Duplicator,
//! }
//!
//! # fn main() {}
//! ```
//!
//! Conversions can be chained too:
//!
//! ```
//! struct ChatId(u64);
//!
//! impl From<u64> for ChatId {
//! fn from(value: u64) -> Self {
//! Self(value)
//! }
//! }
//!
//! micronfig::config! {
//! // First parse the string as an u64 with FromStr, then convert it to a ChatId with From.
//! RESPOND_TO_MESSAGES_IN: String > u64 -> crate::ChatId,
//! }
//!
//! # fn main() {}
//! ```
//!
//! ## Crate features
//!
//! ### Value sources
//!
//! The crate supports retrieving values from various different sources depending on the needs of the application.
//!
//! The sources can be toggled on and off via crate features, and are listed in the following table in order of retrieval priority, where the topmost one is the first source that is checked, and the following ones are checked only if no value is detected in the ones above.
//!
//! | Feature | Description | Use case |
//! |---|---|---|
//! | `envfiles` | Contents of the file at the path indicated by the `{NAME}_FILE` environment variable. | Docker [configs](https://docs.docker.com/engine/swarm/configs/) and [secrets](https://docs.docker.com/engine/swarm/secrets/). |
//! | `envvars` | The `{NAME}` environment variable. | Most command-line applications. |
//! | `envdot` | The `.env` and `.env.local` files in the current working directory. | Application development. |
//!
//! By default, all of them are enabled.
//!
#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/micronfig/main/icon.png")]
/// The macro described at the crate's root.
pub use micronfig_macros::config;
pub mod cache;
#[cfg(feature = "envvars")]
pub mod envvars;
#[cfg(feature = "envfiles")]
pub mod envfiles;
#[cfg(feature = "envdot")]
pub mod envdot;
#[cfg(test)]
pub mod testing;

13
micronfig/src/testing.rs Normal file
View file

@ -0,0 +1,13 @@
/// **Private**; utilities for testing.
pub fn tempfile_fixture(content: &str) -> tempfile::TempPath {
use std::io::Write;
let mut file = tempfile::NamedTempFile::new()
.expect("the tempfile fixture to be created successfully");
write!(file, "{}", content)
.expect("to be able to write into the tempfile fixture");
file.into_temp_path()
}

View file

@ -0,0 +1,24 @@
[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
[dependencies]
syn = "2.0"
quote = "1.0"
[dev-dependencies]
micronfig = { version = "0.3.0", path = "../micronfig" }
trybuild = "1.0.87"
[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>

205
micronfig_macros/src/lib.rs Normal file
View file

@ -0,0 +1,205 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{Ident, parse_macro_input, Token, Type, TypePath};
use syn::punctuated::Punctuated;
type Config = Punctuated<ConfigItem, Token![,]>;
#[derive(Clone)]
struct ConfigItem {
identifier: Ident,
optional: bool,
types: Vec<ConfigPair>,
}
#[derive(Clone)]
struct ConfigPair {
conversion: Conversion,
r#type: Type,
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
enum Conversion {
From,
TryFrom,
FromStr,
}
impl Parse for ConfigItem {
fn parse(input: ParseStream) -> syn::Result<Self> {
let identifier = input.parse::<Ident>()?;
let optional = input.lookahead1().peek(Token![?]);
if optional {
input.parse::<Token![?]>()
.expect("this token to be parsed correctly, as it has been previously peeked");
}
let types = match input.lookahead1().peek(Token![:]) {
true => {
input.parse::<Token![:]>()
.expect("this token to be parsed correctly, as it has been previously peeked");
let string_type = input.parse::<TypePath>()?;
if &*string_type.to_token_stream().to_string() != "String" {
return Err(
syn::Error::new_spanned(
string_type,
"first type of a conversion chain should always be literally `String`, other aliases are not allowed"
)
);
}
let mut types = Vec::new();
while let Ok(typ) = input.parse::<ConfigPair>() {
types.push(typ)
}
types
},
false => Vec::new(),
};
Ok(Self { identifier, optional, types })
}
}
impl Parse for ConfigPair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let conversion = input.parse::<Conversion>()?;
let r#type = input.parse::<Type>()?;
Ok(Self { conversion, r#type })
}
}
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 with syn::punctuated::Punctuated::parse_terminated);
let cache_code = quote! {
#[allow(non_snake_case)]
mod _cache {
pub static _lock: std::sync::OnceLock<micronfig::cache::Cache> = std::sync::OnceLock::new();
}
#[allow(non_snake_case)]
fn _cache() -> &'static micronfig::cache::Cache {
_cache::_lock.get_or_init(micronfig::cache::Cache::new)
}
};
let items_code = input.iter().map(|item: &ConfigItem| {
let identifier = &item.identifier;
let identifier_string = identifier.to_string();
let type_final = match item.types.last() {
Some(pair) => {
let typ = pair.r#type.clone();
quote! { #typ }
},
None => {
quote! { std::string::String }
},
};
let type_final_option = match item.optional {
true => quote! { std::option::Option<#type_final> },
false => quote! { #type_final },
};
let conversion_code = item.types.iter().map(
|ConfigPair { r#type, conversion }| {
let typ = r#type;
match (conversion, item.optional) {
(Conversion::From, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.into());
},
(Conversion::TryFrom, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.try_into())
.map(|v| v.expect(&format!("to be able to convert {}", #identifier_string)));
},
(Conversion::FromStr, true) => quote! {
let value: Option<#typ> = value
.map(|v| v.parse())
.map(|v| v.expect(&format!("to be able to parse {}", #identifier_string)));
},
(Conversion::From, false) => quote! {
let value: #typ = value
.into();
},
(Conversion::TryFrom, false) => quote! {
let value: #typ = value
.try_into()
.expect(&format!("to be able to convert {}", #identifier_string));
},
(Conversion::FromStr, false) => quote! {
let value: #typ = value
.parse()
.expect(&format!("to be able to parse {}", #identifier_string));
},
}
}
).reduce(|acc, new| {
quote! { #acc #new }
});
let require_code = match item.optional {
true => quote! {},
false => quote! {
let value: String = value
.expect(&format!("that configuration variable {} was set", #identifier_string));
},
};
quote! {
#[allow(non_snake_case)]
mod #identifier {
pub(super) static _lock: std::sync::OnceLock<#type_final_option> = std::sync::OnceLock::new();
}
#[allow(non_snake_case)]
pub(crate) fn #identifier() -> &'static #type_final_option {
#identifier::_lock.get_or_init(|| {
let key = #identifier_string.as_ref();
let value: Option<std::string::String> = _cache().get(key);
#require_code
#conversion_code
value
})
}
}
}).reduce(|acc, new| {
quote! { #acc #new }
});
let quote = quote! {
#cache_code
#items_code
};
quote.into()
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > u128 => u64 => u32 => u16 => u8,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
assert_eq!(GARASAUTO(), &1u8);
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > u8 -> u16 -> u32 -> u64 -> u128,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
assert_eq!(GARASAUTO(), &1u128);
}

View file

@ -0,0 +1,7 @@
micronfig::config! {
}
fn main() {
}

View file

@ -0,0 +1,53 @@
use std::env;
#[derive(Debug, PartialEq, Eq)]
struct GuildId(u64);
#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);
impl From<u64> for GuildId {
fn from(value: u64) -> Self {
Self(value)
}
}
impl From<u64> for UserId {
fn from(value: u64) -> Self {
Self(value)
}
}
micronfig::config! {
ANGY_TOKEN: String,
ANGY_APPID: String > u64,
ANGY_PLEX_SERVER: String,
ANGY_PLEX_TOKEN: String,
ANGY_PLEX_LIBRARY: String,
ANGY_PLEX_REPLACE_FROM: String,
ANGY_PLEX_REPLACE_TO: String,
ANGY_DEV_GUILD_ID?: String > u64 -> crate::GuildId,
ANGY_DEV_USER_ID?: String > u64 -> crate::UserId,
}
fn main() {
env::set_var("ANGY_TOKEN", "abcdef");
env::set_var("ANGY_APPID", "1234");
env::set_var("ANGY_PLEX_SERVER", "example.org");
env::set_var("ANGY_PLEX_TOKEN", "123456dada");
env::set_var("ANGY_PLEX_LIBRARY", "beta");
env::set_var("ANGY_PLEX_REPLACE_FROM", "sus");
env::set_var("ANGY_PLEX_REPLACE_TO", "sos");
env::set_var("ANGY_DEV_GUILD_ID", "4567");
env::set_var("ANGY_DEV_USER_ID", "5678");
assert_eq!(ANGY_TOKEN(), "abcdef");
assert_eq!(ANGY_APPID(), &1234);
assert_eq!(ANGY_PLEX_SERVER(), "example.org");
assert_eq!(ANGY_PLEX_TOKEN(), "123456dada");
assert_eq!(ANGY_PLEX_LIBRARY(), "beta");
assert_eq!(ANGY_PLEX_REPLACE_FROM(), "sus");
assert_eq!(ANGY_PLEX_REPLACE_TO(), "sos");
assert_eq!(ANGY_DEV_GUILD_ID(), &Some(GuildId(4567)));
assert_eq!(ANGY_DEV_USER_ID(), &Some(UserId(5678)));
}

View file

@ -0,0 +1,17 @@
use std::net::{SocketAddr, SocketAddrV4, Ipv4Addr};
micronfig::config! {
REDIS_CONN: String,
AXUM_HOST: String > std::net::SocketAddr,
CREATE_TOKEN: String,
}
fn main() {
std::env::set_var("REDIS_CONN", "redis://garas");
std::env::set_var("AXUM_HOST", "127.0.0.1:12345");
std::env::set_var("CREATE_TOKEN", "tokennnnn");
assert_eq!(REDIS_CONN(), "redis://garas");
assert_eq!(AXUM_HOST(), &SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 12345)), "127.0.0.1:12345");
assert_eq!(CREATE_TOKEN(), "tokennnnn");
}

View file

@ -0,0 +1,34 @@
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq)]
pub struct CommaSeparatedStrings(Vec<String>);
impl FromStr for CommaSeparatedStrings {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.split(',').map(|v| v.to_string()).collect()))
}
}
micronfig::config! {
DATA_DRAGON_LOCALE: String,
DATA_DRAGON_SET_CODES: String > crate::CommaSeparatedStrings,
POROXY_KEY: String,
POROXY_SALT: String,
SERENITY_DEV_GUILD_ID?: String > u64,
}
fn main() {
std::env::set_var("DATA_DRAGON_LOCALE", "it_IT");
std::env::set_var("DATA_DRAGON_SET_CODES", "set1,set2abc");
std::env::set_var("POROXY_KEY", "abcdef");
std::env::set_var("POROXY_SALT", "abcdef");
std::env::remove_var("SERENITY_DEV_GUILD_ID");
assert_eq!(DATA_DRAGON_LOCALE(), "it_IT");
assert_eq!(DATA_DRAGON_SET_CODES(), &CommaSeparatedStrings(vec!["set1".to_string(), "set2abc".to_string()]));
assert_eq!(POROXY_KEY(), "abcdef");
assert_eq!(POROXY_SALT(), "abcdef");
assert_eq!(SERENITY_DEV_GUILD_ID(), &None);
}

View file

@ -0,0 +1,17 @@
#[derive(Debug, PartialEq, Eq)]
struct MyCustomStruct(String);
impl From<String> for MyCustomStruct {
fn from(value: String) -> Self {
Self(value)
}
}
micronfig::config! {
GARASAUTO: String -> crate::MyCustomStruct,
}
fn main() {
std::env::set_var("GARASAUTO", "baba");
assert_eq!(GARASAUTO(), &MyCustomStruct("baba".to_string()));
}

View file

@ -0,0 +1,19 @@
#[derive(Debug, PartialEq, Eq)]
struct MyCustomStruct(String);
impl std::str::FromStr for MyCustomStruct {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_owned()))
}
}
micronfig::config! {
GARASAUTO: String > crate::MyCustomStruct,
}
fn main() {
std::env::set_var("GARASAUTO", "keke");
assert_eq!(GARASAUTO(), &MyCustomStruct("keke".to_string()));
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > i64,
}
fn main() {
std::env::set_var("GARASAUTO", "-1");
assert_eq!(GARASAUTO(), &(-1i64));
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > std::path::PathBuf,
}
fn main() {
std::env::set_var("GARASAUTO", "./garas");
assert_eq!(GARASAUTO(), &std::path::PathBuf::from("./garas"));
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
assert_eq!(GARASAUTO(), &1u64);
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO?: String > u64,
}
fn main() {
std::env::remove_var("GARASAUTO");
assert_eq!(GARASAUTO(), &None);
}

View file

@ -0,0 +1,14 @@
micronfig::config! {
GARAS: String,
AUTO: String,
BUS: String,
}
fn main() {
std::env::set_var("GARAS", "garas");
std::env::set_var("AUTO", "auto");
std::env::set_var("BUS", "bus");
assert_eq!(GARAS(), "garas");
assert_eq!(AUTO(), "auto");
assert_eq!(BUS(), "bus");
}

View file

@ -0,0 +1,14 @@
micronfig::config! {
GARAS,
AUTO,
BUS,
}
fn main() {
std::env::set_var("GARAS", "garas");
std::env::set_var("AUTO", "auto");
std::env::set_var("BUS", "bus");
assert_eq!(GARAS(), "garas");
assert_eq!(AUTO(), "auto");
assert_eq!(BUS(), "bus");
}

View file

@ -0,0 +1,14 @@
micronfig::config! {
GARAS,
AUTO: String,
BUS,
}
fn main() {
std::env::set_var("GARAS", "garas");
std::env::set_var("AUTO", "auto");
std::env::set_var("BUS", "bus");
assert_eq!(GARAS(), "garas");
assert_eq!(AUTO(), "auto");
assert_eq!(BUS(), "bus");
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String,
}
fn main() {
std::env::set_var("GARASAUTO", "sagramoto");
assert_eq!(GARASAUTO(), "sagramoto");
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO,
}
fn main() {
std::env::set_var("GARASAUTO", "fieraereo");
assert_eq!(GARASAUTO(), "fieraereo");
}

View file

@ -0,0 +1,19 @@
#[derive(Debug, PartialEq, Eq)]
struct MyCustomStruct(String);
impl std::convert::TryFrom<String> for MyCustomStruct {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self(value))
}
}
micronfig::config! {
GARASAUTO: String => crate::MyCustomStruct,
}
fn main() {
std::env::set_var("GARASAUTO", "me");
assert_eq!(GARASAUTO(), &MyCustomStruct("me".to_string()));
}

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String ==> u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: expected `,`
--> tests/sources/wrong_conversion_longfatarrow.rs:2:20
|
2 | GARASAUTO: String ==> u64,
| ^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_conversion_longfatarrow.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String --> u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: expected `,`
--> tests/sources/wrong_conversion_longthinarrow.rs:2:20
|
2 | GARASAUTO: String --> u64,
| ^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_conversion_longthinarrow.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String ~> u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: expected `,`
--> tests/sources/wrong_conversion_tildearrow.rs:2:20
|
2 | GARASAUTO: String ~> u64,
| ^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_conversion_tildearrow.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String -> u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
println!("{:#?}", GARASAUTO())
}

View file

@ -0,0 +1,24 @@
error[E0277]: the trait bound `u64: From<String>` is not satisfied
--> tests/sources/wrong_conversion_trait_from.rs:1:1
|
1 | / micronfig::config! {
2 | | GARASAUTO: String -> u64,
3 | | }
| | ^
| | |
| |_the trait `From<String>` is not implemented for `u64`
| in this macro invocation
|
::: src/lib.rs
|
| pub fn config(input: TokenStream) -> TokenStream {
| ------------------------------------------------ in this expansion of `micronfig::config!`
|
= help: the following other types implement trait `From<T>`:
<u64 as From<bool>>
<u64 as From<char>>
<u64 as From<u8>>
<u64 as From<u16>>
<u64 as From<u32>>
<u64 as From<NonZeroU64>>
= note: required for `String` to implement `Into<u64>`

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > std::convert::Infallible,
}
fn main() {
std::env::set_var("GARASAUTO", "!");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,28 @@
error[E0277]: the trait bound `Infallible: FromStr` is not satisfied
--> tests/sources/wrong_conversion_trait_fromstr.rs:1:1
|
1 | / micronfig::config! {
2 | | GARASAUTO: String > std::convert::Infallible,
3 | | }
| | ^
| | |
| |_the trait `FromStr` is not implemented for `Infallible`
| in this macro invocation
|
::: src/lib.rs
|
| pub fn config(input: TokenStream) -> TokenStream {
| ------------------------------------------------ in this expansion of `micronfig::config!`
|
= help: the following other types implement trait `FromStr`:
bool
char
isize
i8
i16
i32
i64
i128
and $N others
note: required by a bound in `core::str::<impl str>::parse`
--> $RUST/core/src/str/mod.rs

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String => u64,
}
fn main() {
std::env::set_var("GARASAUTO", "1");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,26 @@
error[E0277]: the trait bound `u64: From<String>` is not satisfied
--> tests/sources/wrong_conversion_trait_tryfrom.rs:1:1
|
1 | / micronfig::config! {
2 | | GARASAUTO: String => u64,
3 | | }
| | ^
| | |
| |_the trait `From<String>` is not implemented for `u64`
| in this macro invocation
|
::: src/lib.rs
|
| pub fn config(input: TokenStream) -> TokenStream {
| ------------------------------------------------ in this expansion of `micronfig::config!`
|
= help: the following other types implement trait `From<T>`:
<u64 as From<bool>>
<u64 as From<char>>
<u64 as From<u8>>
<u64 as From<u16>>
<u64 as From<u32>>
<u64 as From<NonZeroU64>>
= note: required for `String` to implement `Into<u64>`
= note: required for `u64` to implement `TryFrom<String>`
= note: required for `String` to implement `TryInto<u64>`

View file

@ -0,0 +1,7 @@
micronfig::config! {
garasauto!"68"3469l
}
fn main() {
}

View file

@ -0,0 +1,5 @@
error: expected `,`
--> tests/sources/wrong_nonsense_1.rs:2:11
|
2 | garasauto!"68"3469l
| ^

View file

@ -0,0 +1,7 @@
micronfig::config! {
: ->
}
fn main() {
}

View file

@ -0,0 +1,5 @@
error: expected identifier
--> tests/sources/wrong_nonsense_2.rs:2:2
|
2 | : ->
| ^

View file

@ -0,0 +1,7 @@
micronfig::config! {
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
}
fn main() {
}

View file

@ -0,0 +1,5 @@
error: expected identifier
--> tests/sources/wrong_nonsense_3.rs:2:2
|
2 | ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
| ^

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: i64,
}
fn main() {
std::env::set_var("GARASAUTO", "-1");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: first type of a conversion chain should always be literally `String`, other aliases are not allowed
--> tests/sources/wrong_start.rs:2:13
|
2 | GARASAUTO: i64,
| ^^^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_start.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: ,
}
fn main() {
std::env::set_var("GARASAUTO", "garasauto");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: expected identifier
--> tests/sources/wrong_syntax_colon.rs:2:13
|
2 | GARASAUTO: ,
| ^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_syntax_colon.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO String,
}
fn main() {
std::env::set_var("GARASAUTO", "garasauto");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,11 @@
error: expected `,`
--> tests/sources/wrong_syntax_type.rs:2:12
|
2 | GARASAUTO String,
| ^^^^^^
error[E0425]: cannot find function, tuple struct or tuple variant `GARASAUTO` in this scope
--> tests/sources/wrong_syntax_type.rs:7:20
|
7 | println!("{:#?}", GARASAUTO());
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,10 @@
use std::path::PathBuf;
micronfig::config! {
GARASAUTO: String > PathBuf,
}
fn main() {
std::env::set_var("GARASAUTO", "./auto");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,9 @@
error[E0412]: cannot find type `PathBuf` in this scope
--> tests/sources/wrong_unqualified_import.rs:4:22
|
4 | GARASAUTO: String > PathBuf,
| ^^^^^^^ not found in this scope
|
= help: consider importing one of these items:
crate::PathBuf
std::path::PathBuf

View file

@ -0,0 +1,8 @@
micronfig::config! {
GARASAUTO: String > PathBuf,
}
fn main() {
std::env::set_var("GARASAUTO", "./bus");
println!("{:#?}", GARASAUTO());
}

View file

@ -0,0 +1,19 @@
error[E0412]: cannot find type `PathBuf` in this scope
--> tests/sources/wrong_unqualified_noimport.rs:2:22
|
2 | GARASAUTO: String > PathBuf,
| ^^^^^^^ not found in this scope
|
= help: consider importing this struct:
std::path::PathBuf
error[E0412]: cannot find type `PathBuf` in this scope
--> tests/sources/wrong_unqualified_noimport.rs:2:22
|
2 | GARASAUTO: String > PathBuf,
| ^^^^^^^ not found in this scope
|
help: consider importing this struct
|
1 + use std::path::PathBuf;
|

View file

@ -0,0 +1,51 @@
macro_rules! pass {
($id:ident) => {
#[test]
fn $id() {
trybuild::TestCases::new().pass(format!("tests/sources/{}.rs", stringify!($id)));
}
}
}
macro_rules! fail {
($id:ident) => {
#[test]
fn $id() {
trybuild::TestCases::new().compile_fail(format!("tests/sources/{}.rs", stringify!($id)));
}
}
}
pass!(chain_single_down);
pass!(chain_single_up);
pass!(empty);
pass!(example_angybot);
pass!(example_distributedarcade);
pass!(example_patchedporobot);
pass!(from_single_custom);
pass!(parse_single_custom);
pass!(parse_single_i64);
pass!(parse_single_pathbuf);
pass!(parse_single_u64);
pass!(parse_single_u64_optional);
pass!(string_multi_explicit);
pass!(string_multi_implicit);
pass!(string_multi_mixed);
pass!(string_single_explicit);
pass!(string_single_implicit);
pass!(tryfrom_single_custom);
fail!(wrong_conversion_longfatarrow);
fail!(wrong_conversion_longthinarrow);
fail!(wrong_conversion_tildearrow);
fail!(wrong_conversion_trait_from);
fail!(wrong_conversion_trait_fromstr);
fail!(wrong_conversion_trait_tryfrom);
fail!(wrong_nonsense_1);
fail!(wrong_nonsense_2);
fail!(wrong_nonsense_3);
fail!(wrong_start);
fail!(wrong_syntax_colon);
fail!(wrong_syntax_type);
fail!(wrong_unqualified_import);
fail!(wrong_unqualified_noimport);

View file

@ -1,153 +0,0 @@
//! High-level API — Handle errors automatically.
//!
//! It can be useful if you want to specify when configuration values are loaded in the lifecycle of your binary.
/// Get a value from the first available source, panicking with a human-readable message in case the value is missing or cannot be processed.
///
/// # Process
///
/// This function:
///
/// 1. calls [`crate::multi::get`] with the given key and a file suffix of `_FILE`
/// 2. pattern matches errors and [`panic`]s if an error is caught.
///
/// # Panics
///
/// Any error encountered by this function causes a panic with a message describing what went wrong.
///
/// The same thing happens if the configuration value could not be retrieved by any source.
///
/// # Examples
///
/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]:
/// ```
/// use micronfig::handle::get_required;
/// #
/// # std::env::set_var("USER", "steffo");
/// # std::env::remove_var("USER_FILE");
///
/// let user: String = get_required("USER");
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// use micronfig::handle::get_required;
/// #
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// # std::env::remove_var("IP_ADDRESS_FILE");
///
/// let ip_addr: IpAddr = get_required("IP_ADDRESS");
/// ```
///
/// # See also
///
/// [`get_optional`], which has the same behaviour but does not panic if the value is not found, instead returning [`None`].
///
/// # Possible future improvements
///
/// Possibly refactor this to a method of [`crate::multi::Source`].
///
pub fn get_required<Type>(key: &str) -> Type
where Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
use crate::multi::{get, Source};
use crate::single::{envvars, envfiles};
match get(key, "_FILE") {
Source::EnvVar(Ok(v)) => v,
Source::EnvVar(Err(envvars::Error::CannotConvertValue(err))) =>
panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &key, &std::any::type_name::<Type>(), &err),
Source::EnvVar(Err(envvars::Error::CannotReadEnvVar(_))) =>
panic!("Something unexpected happened in micronfig. Please report this as a bug!"),
Source::EnvFile(Ok(v)) => v,
Source::EnvFile(Err(envfiles::Error::CannotConvertValue(err))) =>
panic!("The contents of the file at {} could not be converted to a {}: {:?}", &key, &std::any::type_name::<Type>(), &err),
Source::EnvFile(Err(envfiles::Error::CannotOpenFile(err))) =>
panic!("The file at {} could not be opened: {}", &key, &err),
Source::EnvFile(Err(envfiles::Error::CannotReadFile(err))) =>
panic!("The contents of the file at {} could not be read: {}", &key, &err),
Source::EnvFile(Err(envfiles::Error::CannotReadEnvVar(_))) =>
panic!("Something unexpected happened in micronfig. Please report this as a bug!"),
Source::NotFound =>
panic!("The configuration value {} is not defined.", &key),
}
}
/// Try to get a value from the first available source, panicking with a human-readable message in case it cannot be processed.
///
/// # Process
///
/// This function:
///
/// 1. calls [`crate::multi::get`] with the given key and a file suffix of `_FILE`
/// 2. pattern matches errors and [`panic`]s if an error is caught.
///
/// # Panics
///
/// Any error encountered by this function causes a panic with a message describing what went wrong.
///
/// # Examples
///
/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]:
/// ```
/// use micronfig::handle::get_optional;
/// #
/// # std::env::set_var("USER", "steffo");
/// # std::env::remove_var("USER_FILE");
///
/// let user: Option<String> = get_optional("USER");
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// use micronfig::handle::get_optional;
/// #
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// # std::env::remove_var("IP_ADDRESS_FILE");
///
/// let ip_addr: Option<IpAddr> = get_optional("IP_ADDRESS");
/// ```
///
/// # See also
///
/// [`get_required`], which has the same behaviour but does panics if the value is not found.
///
/// # Possible future improvements
///
/// Possibly refactor this to a method of [`crate::multi::Source`].
///
pub fn get_optional<Type>(name: &str) -> Option<Type>
where Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
use crate::multi::{get, Source};
use crate::single::{envvars, envfiles};
match get(name, "_FILE") {
Source::EnvVar(Ok(v)) => Some(v),
Source::EnvVar(Err(envvars::Error::CannotConvertValue(err))) =>
panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &name, &std::any::type_name::<Type>(), &err),
Source::EnvVar(Err(envvars::Error::CannotReadEnvVar(_))) =>
panic!("Something unexpected happened in micronfig. Please report this as a bug!"),
Source::EnvFile(Ok(v)) => Some(v),
Source::EnvFile(Err(envfiles::Error::CannotConvertValue(err))) =>
panic!("The contents of the file at {} could not be converted to a {}: {:?}", &name, &std::any::type_name::<Type>(), &err),
Source::EnvFile(Err(envfiles::Error::CannotOpenFile(err))) =>
panic!("The file at {} could not be opened: {}", &name, &err),
Source::EnvFile(Err(envfiles::Error::CannotReadFile(err))) =>
panic!("The contents of the file at {} could not be read: {}", &name, &err),
Source::EnvFile(Err(envfiles::Error::CannotReadEnvVar(_))) =>
panic!("Something unexpected happened in micronfig. Please report this as a bug!"),
Source::NotFound => None,
}
}

View file

@ -1,52 +0,0 @@
//! Tiny crate for simple configuration management.
//!
//! > **Unstable**; I haven't fully committed to the API yet, so it might change wildly in the following minor versions (`0.x.0`).
//!
//! # Features
//!
//! This crate handles:
//!
//! - Retrieval of values of configuration properties from multiple sources, such as environment variables or files
//! - Parsing of retrieved data
//! - Displaying human-readable errors if case a step does not succeed
//!
//! # Usage
//!
//! This crate has four levels of abstraction, each one with a different usage method.
//!
//! In order from the highest to the lowest, they are:
//!
//! 1. **Recommended**: [`required`] and [`optional`], macros which allow you to define global, lazily-evaluated, configuration values;
//! 2. [`handle::get_required`] and [`handle::get_optional`], functions which allow you to get a configuration value in a specific moment, without having to consider handling errors;
//! 3. [`multi::get`], function which behaves in the same way as the previous two, but returns [`multi::Source`] instead, allowing you to handle errors how you prefer;
//! 4. [`single`], module containing submodules allowing the retrieval of configuration values from a single source, returning a source-specific [`Result`].
//!
//! ## Examples
//!
//! Some examples are provided in the crate source, [inside the `examples/` directory](https://github.com/Steffo99/micronfig/tree/main/examples).
#![warn(missing_docs)]
#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/micronfig/main/icon.png")]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod single;
#[cfg(feature = "multi")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "multi")))]
pub mod multi;
#[cfg(feature = "handle")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "handle")))]
pub mod handle;
#[cfg(feature = "macros")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "macros")))]
pub use lazy_static;
#[cfg(feature = "macros")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "macros")))]
pub mod macros;
#[cfg(feature = "testing")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "testing")))]
pub mod testing;

View file

@ -1,82 +0,0 @@
//! Highest-level API — Define lazy statics.
//!
//! The recommended way to use the library.
/// Define a required configuration value with a certain type.
///
/// # Process
///
/// This macro:
///
/// 1. uses [`lazy_static::lazy_static`] to define a new static variable (and associated struct)
/// 2. uses [`crate::handle::get_required`] to get the configuration value and handle eventual errors
///
/// # Examples
///
/// Define a configuration value with the `USER` key, a [`String`]:
/// ```
/// # std::env::set_var("USER", "steffo");
/// # std::env::remove_var("USER_FILE");
/// #
/// micronfig::required!(USER, String);
/// println!("{:?}", *USER);
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// #
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// # std::env::remove_var("IP_ADDRESS_FILE");
///
/// micronfig::required!(IP_ADDRESS, IpAddr);
/// println!("{:?}", *IP_ADDRESS);
/// ```
#[macro_export]
macro_rules! required {
($identifier:ident, $kind:ty) => {
$crate::lazy_static::lazy_static! {
pub(crate) static ref $identifier: $kind = $crate::handle::get_required::<$kind>(stringify!($identifier));
}
};
}
/// Define a optional configuration value with a certain type.
///
/// # Process
///
/// This macro:
///
/// 1. uses [`lazy_static::lazy_static`] to define a new static variable (and associated struct)
/// 2. uses [`crate::handle::get_optional`] to get the configuration value and handle eventual errors
///
/// # Examples
///
/// Define a configuration value with the `USER` key, a [`String`]:
/// ```
/// # std::env::set_var("USER", "steffo");
/// # std::env::remove_var("USER_FILE");
/// #
/// micronfig::optional!(USER, String);
/// println!("{:?}", *USER);
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// #
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// # std::env::remove_var("IP_ADDRESS_FILE");
///
/// micronfig::optional!(IP_ADDRESS, IpAddr);
/// println!("{:?}", *IP_ADDRESS);
/// ```
#[macro_export]
macro_rules! optional {
($identifier:ident, $kind:ty) => {
$crate::lazy_static::lazy_static! {
pub(crate) static ref $identifier: Option<$kind> = $crate::handle::get_optional::<$kind>(stringify!($identifier));
}
}
}

View file

@ -1,293 +0,0 @@
//! Middle-level API — Use all available configuration sources.
//!
//! It can be useful if you want more control on how errors are handled or on how the key is passed to the [`crate::single`] sources.
use std::ffi::OsString;
#[cfg(feature = "single_envvars")] use crate::single::envvars;
#[cfg(feature = "single_envfiles")] use crate::single::envfiles;
/// Get a value from the first available source, additionally returning information about how the value was retrieved.
///
/// # Process
///
/// This function tries to `get` a configuration value:
///
/// 1. with [`envvars::get`], using `key`, returning a [`Source::EnvVar`]
/// 2. with [`envfiles::get`], using `key + key_suffix_file`, returning a [`Source::EnvFile`]
///
/// If none of these options successfully resulted in the successful retrieval of the configuration value, [`Source::NotFound`] is returned instead.
///
/// # Errors
///
/// All errors encountered are bubbled up, except the ones surfacing because of the total absence of a configuration value, which make the function immediately try the next available source.
///
/// Currently, those errors are:
/// - [`envvars::Error::CannotReadEnvVar`]
/// - [`envfiles::Error::CannotReadEnvVar`]
///
/// # Examples
///
/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]:
/// ```
/// use micronfig::multi::get;
/// use micronfig::multi::Source;
/// #
/// # std::env::set_var("USER", "steffo");
/// # std::env::remove_var("USER_FILE");
///
/// let user: Source<u32> = get("USER", "_FILE");
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// use micronfig::multi::get;
/// use micronfig::multi::Source;
/// #
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// # std::env::remove_var("IP_ADDRESS_FILE");
///
/// let ip_addr: Source<IpAddr> = get("IP_ADDRESS", "_FILE");
/// ```
///
pub fn get<Key, KeySuffixFile, Type>(key: Key, key_suffix_file: KeySuffixFile) -> Source<Type>
where Key: AsRef<std::ffi::OsStr>,
KeySuffixFile: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
if cfg!(feature = "single_envvars") {
let v = envvars::get(&key);
match v {
Err(envvars::Error::CannotReadEnvVar(_)) => {},
_ => return Source::EnvVar(v),
}
}
if cfg!(feature = "single_envfiles") {
let mut key_file = OsString::new();
key_file.push(key);
key_file.push(key_suffix_file);
let v = envfiles::get(key_file);
match v {
Err(envfiles::Error::CannotReadEnvVar(_)) => {},
_ => return Source::EnvFile(v),
}
}
Source::NotFound
}
/// The way the result returned by [`get`] was obtained.
///
/// Since more sources might be added in the future, this function is `non_exaustive`.
#[non_exhaustive]
#[derive(Debug)]
pub enum Source<Type>
where Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
/// The result was not obtained, since the configuration value was not defined anywhere.
NotFound,
/// The result was obtained by [`envvars::get`].
#[cfg(feature = "single_envvars")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envvars")))]
EnvVar(envvars::Result<Type>),
/// The result was obtained by [`envfiles::get`].
#[cfg(feature = "single_envfiles")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envfiles")))]
EnvFile(envfiles::Result<Type>),
}
impl<Type> Source<Type>
where Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
/// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside.
///
/// # Panics
///
/// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`].
///
/// The panic message is the `msg` given.
///
/// # See also
///
/// Similar to [`Result::expect`].
///
/// Used by [`Self::unwrap`].
///
/// # Examples
///
/// ```
/// use micronfig::multi::Source;
///
/// let value = Source::<u8>::EnvFile(Ok(1)).expect("value to be present");
/// assert_eq!(value, 1)
/// ```
///
/// ```should_panic
/// use micronfig::multi::Source;
/// use micronfig::single::envfiles::Error as FileError;
///
/// let value = Source::<u8>::EnvFile(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).expect("value to be present");
/// // Panic!
/// ```
pub fn expect(self, msg: &str) -> Type {
match self {
#[cfg(feature = "single_envvars")]
Self::EnvVar(Ok(v)) => v,
#[cfg(feature = "single_envfiles")]
Self::EnvFile(Ok(v)) => v,
_ => panic!("{}", msg),
}
}
/// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside.
///
/// # Panics
///
/// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`].
///
/// # See also
///
/// Similar to [`Result::unwrap`].
///
/// Internally, it uses [`Self::expect`].
///
/// # Examples
///
/// ```
/// use micronfig::multi::Source;
///
/// let value = Source::<u8>::EnvFile(Ok(1)).unwrap();
/// assert_eq!(value, 1)
/// ```
///
/// ```should_panic
/// use micronfig::multi::Source;
/// use micronfig::single::envfiles::Error as FileError;
///
/// let value = Source::<u8>::EnvFile(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
/// // Panic!
/// ```
pub fn unwrap(self) -> Type
{
self.expect("called `Source::unwrap()` on an invalid variant, such as `NotFound` or `_(Err(_))`")
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::testing::tempfile_fixture;
#[test]
#[cfg(feature = "single_envvars")]
fn it_works_var() {
std::env::set_var("NUMBER", "1");
std::env::remove_var("NUMBER_FILE");
match get::<&str, &str, u32>("NUMBER", "_FILE") {
Source::EnvVar(Ok(1u32)) => {},
_ => panic!("expected Source::EnvVar(Ok(1u32))")
}
}
#[test]
#[cfg(feature = "single_envfiles")]
fn it_works_file() {
let file = tempfile_fixture("1");
std::env::remove_var("NUMBER");
std::env::set_var("NUMBER_FILE", file.as_os_str());
let n = get::<&str, &str, u32>("NUMBER", "_FILE");
match n {
Source::EnvFile(Ok(1u32)) => {},
_ => panic!("expected Source::EnvFile(Ok(1u32))")
}
}
#[test]
#[cfg(feature = "single_envvars")]
fn missing_envvar() {
match get::<&str, &str, String>("MISSING_ENVVAR", "_FILE") {
Source::NotFound => {},
_ => panic!("expected Source::NotFound"),
}
}
#[test]
#[cfg(feature = "single_envfiles")]
fn missing_file() {
std::env::remove_var("NUMBER");
std::env::set_var("NUMBER_FILE", "/this/file/does/not/exist");
match get::<&str, &str, u32>("NUMBER", "_FILE") {
Source::EnvFile(Err(envfiles::Error::CannotOpenFile(_))) => {},
_ => panic!("expected Source::EnvFile(Err(envfiles::Error::CannotOpenFile(_)))"),
}
}
#[test]
#[cfg(feature = "single_envvars")]
fn not_a_number_var() {
std::env::set_var("NUMBER", "XYZ");
std::env::remove_var("NUMBER_FILE");
match get::<&str, &str, u32>("NUMBER", "_FILE") {
Source::EnvVar(Err(envvars::Error::CannotConvertValue(_))) => {},
_ => panic!("expected Source::EnvVar(Err(envvars::Error::CannotConvertValue(_)))"),
}
}
#[test]
#[cfg(feature = "single_envfiles")]
fn not_a_number_file() {
let file = tempfile_fixture("XYZ");
std::env::set_var("NUMBER_FILE", file.as_os_str());
std::env::remove_var("NUMBER");
match get::<&str, &str, u32>("NUMBER", "_FILE") {
Source::EnvFile(Err(envfiles::Error::CannotConvertValue(_))) => {},
_ => panic!("expected Source::EnvFile(Err(envfiles::Error::CannotConvertValue(_)))"),
}
}
#[test]
#[cfg(feature = "single_envvars")]
fn unwrap_var_ok() {
Source::EnvVar(Ok("ok".to_string())).unwrap();
}
#[test]
#[cfg(feature = "single_envfiles")]
fn unwrap_file_ok() {
Source::EnvFile(Ok("ok".to_string())).unwrap();
}
#[test]
#[should_panic]
#[cfg(feature = "single_envvars")]
fn unwrap_var_err() {
Source::<String>::EnvVar(Err(envvars::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
}
#[test]
#[should_panic]
#[cfg(feature = "single_envfiles")]
fn unwrap_file_err() {
Source::<String>::EnvFile(Err(envfiles::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
}
}

View file

@ -1,137 +0,0 @@
//! Contents of files at paths defined by environment variables.
/// Get a configuration value from the source.
///
/// # Process
///
/// This function:
///
/// 1. tries to access the environment variable with the given name using [`std::env::var`]
/// 2. tries to interpret the contents of the environment variable as a [`std::path::PathBuf`]
/// 3. tries to [`std::fs::File::open`] the file at that path
/// 4. tries to [`std::io::Read::read_to_string`] the contents of the opened file
/// 5. tries to convert the obtained value to another of the given type using [`std::str::FromStr::from_str`]
///
/// # Examples
///
/// Retrieve a configuration value from the `USER_FILE` file, maintaining it as a [`String`]:
/// ```
/// use micronfig::single::envfiles::get;
///
/// # let filename = micronfig::testing::tempfile_fixture("steffo");
/// # std::env::set_var("USER_FILE", filename.as_os_str());
/// let user: String = get("USER_FILE").expect("USER_FILE envvar to be defined");
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// use micronfig::single::envfiles::get;
///
/// # let filename = micronfig::testing::tempfile_fixture("192.168.1.1");
/// # std::env::set_var("IP_ADDRESS_FILE", filename.as_os_str());
/// let ip_addr: IpAddr = get("IP_ADDRESS_FILE").expect("IP_ADDRESS_FILE envvar to be defined");
/// ```
///
pub fn get<Key, Type>(key: Key) -> Result<Type>
where Key: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
let path = std::env::var(key)
.map_err(Error::CannotReadEnvVar)?;
let path = std::ffi::OsString::from(path);
let path = std::path::PathBuf::from(path);
let mut file = std::fs::File::open(path)
.map_err(Error::CannotOpenFile)?;
use std::io::Read;
let mut data = String::new();
file.read_to_string(&mut data)
.map_err(Error::CannotReadFile)?;
let value = Type::from_str(&data)
.map_err(Error::CannotConvertValue)?;
Ok(value)
}
/// A possible error encountered by [`get`].
#[derive(std::fmt::Debug)]
pub enum Error<ConversionError>
where ConversionError: std::fmt::Debug,
{
/// The environment variable could not be read.
///
/// Encountered when the call to [`std::env::var`] fails.
CannotReadEnvVar(std::env::VarError),
/// The specified file could not be opened. (Probably it doesn't exist.)
///
/// Encountered when the call to [`std::fs::File::open`] fails.
CannotOpenFile(std::io::Error),
/// The specified file could not be read.
///
/// Encountered when the call to [`std::io::Read::read_to_string`] fails.
CannotReadFile(std::io::Error),
/// The value could not be converted to the desired type.
///
/// Encountered when the call to [`std::str::FromStr::from_str`] fails.
CannotConvertValue(ConversionError),
}
/// A possible error encountered by [`get`].
pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>;
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::testing::tempfile_fixture;
#[test]
fn it_works() {
let file = tempfile_fixture("1");
std::env::set_var("NUMBER_FILE", file.as_os_str());
let number = get::<&str, u32>("NUMBER_FILE").unwrap();
assert_eq!(number, 1u32);
}
#[test]
fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST_FILE");
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST_FILE") {
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),
}
}
#[test]
fn missing_file() {
std::env::set_var("NUMBER_FILE", "/this/file/does/not/exist");
match get::<&str, u32>("NUMBER_FILE") {
Err(Error::CannotOpenFile(_)) => {},
_ => panic!("expected Err(Error::CannotOpenFile(_))"),
}
}
#[test]
fn not_a_number() {
let file = tempfile_fixture("XYZ");
std::env::set_var("NUMBER_FILE", file.as_os_str());
match get::<&str, u32>("NUMBER_FILE") {
Err(Error::CannotConvertValue(_)) => {},
_ => panic!("expected Err(Error::CannotConvertValue(_))"),
}
}
}

View file

@ -1,99 +0,0 @@
//! Environment variables.
/// Get a configuration value from the source.
///
/// # Process
///
/// This function:
///
/// 1. tries to access the environment variable with the given name using [`std::env::var`]
/// 2. tries to convert the obtained value to another of the given type using [`std::str::FromStr::from_str`]
///
/// # Examples
///
/// Retrieve a configuration value from the `USER` environment variable, maintaining it as a [`String`]:
/// ```
/// use micronfig::single::envvars::get;
///
/// # std::env::set_var("USER", "steffo");
/// let user: String = get("USER").expect("USER envvar to be defined");
/// ```
///
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable, then try to convert it to a [`std::net::IpAddr`]:
/// ```
/// use std::net::IpAddr;
/// use micronfig::single::envvars::get;
///
/// # std::env::set_var("IP_ADDRESS", "192.168.1.1");
/// let ip_addr: IpAddr = get("IP_ADDRESS").expect("IP_ADDRESS envvar to be defined");
/// ```
///
pub fn get<Key, Type>(key: Key) -> Result<Type>
where Key: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr,
<Type as std::str::FromStr>::Err: std::fmt::Debug,
{
let data = std::env::var(key)
.map_err(Error::CannotReadEnvVar)?;
let value = Type::from_str(&data)
.map_err(Error::CannotConvertValue)?;
Ok(value)
}
/// A possible error encountered by [`get`].
#[derive(std::fmt::Debug)]
pub enum Error<ConversionError>
where ConversionError: std::fmt::Debug,
{
/// The environment variable could not be read.
///
/// Encountered when the call to [`std::env::var`] fails.
CannotReadEnvVar(std::env::VarError),
/// The value could not be converted to the desired type.
///
/// Encountered when the call to [`std::str::FromStr::from_str`] fails.
CannotConvertValue(ConversionError),
}
/// The result of [`get`].
pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>;
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[test]
fn it_works() {
std::env::set_var("NUMBER", "1");
let number = get::<&str, u32>("NUMBER").unwrap();
assert_eq!(number, 1u32);
}
#[test]
fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST");
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST") {
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),
}
}
#[test]
fn not_a_number() {
std::env::set_var("NUMBER", "XYZ");
match get::<&str, u32>("NUMBER") {
Err(Error::CannotConvertValue(_)) => {},
_ => panic!("expected Error::CannotConvertValue(_)"),
}
}
}

View file

@ -1,15 +0,0 @@
//! Lowest-level API — Manually select configuration sources.
//!
//! It can be useful if you want to specify manually the sources to access when retrieving configuration values.
//!
//! Each possible source has an associated module, and a feature named `single_{MODULENAME}` enabling it; see the list of modules below to see what sources are available!
#[cfg(feature = "single_envvars")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envvars")))]
pub mod envvars;
#[cfg(feature = "single_envfiles")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envfiles")))]
pub mod envfiles;

View file

@ -1,17 +0,0 @@
//! Fixtures for testing.
//!
//! **Unstable**; not supposed to be used outside this crate; do not add `pub(crate)` or doctests will stop working.
/// Create a temporary file and write `content` inside it.
///
/// The file will be deleted as soon as the [`tempfile::TempPath`] is dropped.
pub fn tempfile_fixture(content: &str) -> tempfile::TempPath {
use std::io::Write;
let mut file = tempfile::NamedTempFile::new()
.expect("the tempfile fixture to be created successfully");
write!(file, "{}", content)
.expect("to be able to write into the tempfile fixture");
file.into_temp_path()
}

View file

@ -1,17 +0,0 @@
use std::env;
micronfig::required!(PLAYER_NAME, String);
micronfig::required!(PLAYER_ID, u64);
micronfig::optional!(IS_SUS, bool);
#[test]
fn test_macros() {
env::set_var("PLAYER_NAME", "Steffo");
env::set_var("PLAYER_ID", "1234");
env::remove_var("IS_SUS");
assert_eq!(*PLAYER_NAME, "Steffo");
assert_eq!(*PLAYER_ID, 1234u64);
assert_eq!(*IS_SUS, None);
}

View file

@ -1 +0,0 @@
pub mod macros;